변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
6.7 KiB
Python
189 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
env_config fat 컬럼 정리 — 전략별 config_* 로 이전된 컬럼 제거.
|
|
|
|
1) 백업 (JSON): merged env, env_config 전체 행, 삭제 대상 KV, config_*, env_config_ext
|
|
2) env_config 를 ENV_GLOBAL_KEYS 만 가진 슬림 테이블로 교체
|
|
3) _migrate_config_table_columns 로 누락 글로벌 컬럼 추가
|
|
|
|
실행:
|
|
cd ~/kis_bot && python3 -m kis_trader.scripts.prune_env_config_strategy_columns
|
|
cd ~/kis_bot && python3 -m kis_trader.scripts.prune_env_config_strategy_columns --dry-run
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Set
|
|
|
|
_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_ROOT))
|
|
|
|
from config_schema import classify_config_key # noqa: E402
|
|
from database import ENV_CONFIG_KEYS, ENV_GLOBAL_KEYS, TradeDB # noqa: E402
|
|
|
|
|
|
def _row_to_dict(row: Any) -> Dict[str, Any]:
|
|
if row is None:
|
|
return {}
|
|
if hasattr(row, "keys"):
|
|
return {k: row[k] for k in row.keys()}
|
|
return dict(row)
|
|
|
|
|
|
def _json_safe(val: Any) -> Any:
|
|
if val is None:
|
|
return None
|
|
if isinstance(val, (int, float, bool, str)):
|
|
return val
|
|
return str(val)
|
|
|
|
|
|
def backup_all(db: TradeDB, out_dir: Path) -> Dict[str, Any]:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
meta: Dict[str, Any] = {"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
|
|
merged = db.get_merged_env_snapshot() or {}
|
|
(out_dir / "merged_env_snapshot.json").write_text(
|
|
json.dumps(merged, ensure_ascii=False, indent=2), encoding="utf-8",
|
|
)
|
|
meta["merged_keys"] = len(merged)
|
|
|
|
phys_cols = set(db.conn.get_columns("env_config"))
|
|
phys_cols -= {"id", "created_at"}
|
|
global_set = set(ENV_GLOBAL_KEYS)
|
|
drop_cols = sorted(c for c in phys_cols if c not in global_set)
|
|
keep_cols = [k for k in ENV_GLOBAL_KEYS if k in phys_cols]
|
|
meta["physical_cols"] = len(phys_cols)
|
|
meta["keep_cols"] = len(keep_cols)
|
|
meta["drop_cols"] = len(drop_cols)
|
|
(out_dir / "drop_column_names.json").write_text(
|
|
json.dumps(drop_cols, ensure_ascii=False, indent=2), encoding="utf-8",
|
|
)
|
|
|
|
rows = db.conn.execute("SELECT * FROM env_config ORDER BY id").fetchall()
|
|
full_rows: List[Dict[str, Any]] = []
|
|
dropped_kv: List[Dict[str, Any]] = []
|
|
for raw in rows or []:
|
|
row = _row_to_dict(raw)
|
|
rid = row.get("id")
|
|
full_rows.append({k: _json_safe(v) for k, v in row.items()})
|
|
kv: Dict[str, Any] = {"id": rid, "created_at": row.get("created_at")}
|
|
for col in drop_cols:
|
|
v = row.get(col)
|
|
if v is not None and str(v).strip() != "":
|
|
kv[col] = _json_safe(v)
|
|
if len(kv) > 2:
|
|
dropped_kv.append(kv)
|
|
(out_dir / "env_config_full_rows.json").write_text(
|
|
json.dumps(full_rows, ensure_ascii=False, indent=2), encoding="utf-8",
|
|
)
|
|
(out_dir / "env_config_dropped_columns_kv.json").write_text(
|
|
json.dumps(dropped_kv, ensure_ascii=False, indent=2), encoding="utf-8",
|
|
)
|
|
meta["env_config_rows"] = len(full_rows)
|
|
|
|
for tbl in (
|
|
"config_scalp", "config_short", "config_momentum", "config_breakout",
|
|
"config_range_break", "config_updow", "config_dbband", "env_config_ext",
|
|
):
|
|
try:
|
|
trows = db.conn.execute(f"SELECT * FROM {tbl}").fetchall()
|
|
data = [_row_to_dict(r) for r in (trows or [])]
|
|
(out_dir / f"{tbl}.json").write_text(
|
|
json.dumps(data, ensure_ascii=False, indent=2, default=str),
|
|
encoding="utf-8",
|
|
)
|
|
meta[f"{tbl}_rows"] = len(data)
|
|
except Exception as ex:
|
|
meta[f"{tbl}_error"] = str(ex)
|
|
|
|
(out_dir / "backup_meta.json").write_text(
|
|
json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8",
|
|
)
|
|
return meta
|
|
|
|
|
|
def prune_env_config(db: TradeDB, dry_run: bool = False) -> None:
|
|
phys = set(db.conn.get_columns("env_config")) - {"id", "created_at"}
|
|
keep_in_phys = [k for k in ENV_GLOBAL_KEYS if k in phys]
|
|
if not keep_in_phys:
|
|
raise RuntimeError("env_config 에 유지할 글로벌 컬럼 없음")
|
|
|
|
cols_sql = ", ".join(f"`{k}`" for k in keep_in_phys)
|
|
gcols_all = ", ".join(f"`{k}` TEXT" for k in ENV_GLOBAL_KEYS)
|
|
|
|
print(f"유지 컬럼 {len(keep_in_phys)} / ENV_GLOBAL 전체 {len(ENV_GLOBAL_KEYS)}")
|
|
print(f"삭제 컬럼 {len(phys - set(ENV_GLOBAL_KEYS))} 개")
|
|
|
|
if dry_run:
|
|
print("[dry-run] 슬림 교체 스킵")
|
|
return
|
|
|
|
db.conn.execute("DROP TABLE IF EXISTS env_config_new")
|
|
db.conn.execute(
|
|
f"CREATE TABLE env_config_new ("
|
|
f"id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, "
|
|
f"created_at VARCHAR(30) NOT NULL, {gcols_all}"
|
|
f") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
|
)
|
|
db.conn.execute(
|
|
f"INSERT INTO env_config_new (created_at, {cols_sql}) "
|
|
f"SELECT created_at, {cols_sql} FROM env_config",
|
|
)
|
|
cnt = db.conn.execute("SELECT COUNT(*) AS c FROM env_config_new").fetchone()
|
|
n = cnt["c"] if isinstance(cnt, dict) else cnt[0]
|
|
print(f"env_config_new 적재 {n}행")
|
|
|
|
db.conn.execute("DROP TABLE env_config")
|
|
db.conn.execute("RENAME TABLE env_config_new TO env_config")
|
|
print("env_config 슬림 교체 완료")
|
|
|
|
if hasattr(db, "_env_config_cols_cache"):
|
|
db._env_config_cols_cache = None
|
|
db._migrate_config_table_columns()
|
|
|
|
try:
|
|
from kis_trader.utils.env import invalidate_merged_env_cache
|
|
invalidate_merged_env_cache()
|
|
except Exception:
|
|
pass
|
|
|
|
after = len(db.conn.get_columns("env_config"))
|
|
print(f"env_config 컬럼 수: {after} (id/created_at 포함)")
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="env_config 전략 컬럼 백업 후 제거")
|
|
ap.add_argument("--dry-run", action="store_true", help="백업만, 테이블 교체 안 함")
|
|
ap.add_argument(
|
|
"--backup-dir",
|
|
default="",
|
|
help="백업 디렉터리 (기본: backups/env_config_prune_YYYYMMDD_HHMMSS)",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
out_dir = Path(args.backup_dir) if args.backup_dir else _ROOT / "backups" / f"env_config_prune_{ts}"
|
|
|
|
db = TradeDB()
|
|
try:
|
|
print(f"백업 → {out_dir}")
|
|
meta = backup_all(db, out_dir)
|
|
print(json.dumps(meta, ensure_ascii=False, indent=2))
|
|
prune_env_config(db, dry_run=args.dry_run)
|
|
if not args.dry_run:
|
|
snap = db.get_merged_env_snapshot()
|
|
print(f"병합 snapshot 키 수: {len(snap)} (기능 유지 확인)")
|
|
finally:
|
|
db.close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|