Files
kis_bot/kis_trader/backtest/optuna_common.py
Your Name fc27e726f9 feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes):

구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함.

스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함.

코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함.

시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함.

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
2026-07-17 01:09:09 +09:00

212 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""
kis_trader/backtest/optuna_common.py — Optuna storage·DB 공통 (MariaDB 141)
=========================================================================
TradeDB(database.py) 와 동일 호스트·계정, 전용 DB kis_optuna 에 study 저장.
Win11·VM 양쪽에서 같은 storage 로 trial 공유·재개 가능.
"""
from __future__ import annotations
import logging
import os
from typing import Any, Optional
from urllib.parse import quote_plus
from kis_trader.utils.env import get_env_from_db
logger = logging.getLogger("optuna_common")
# Optuna 전용 MariaDB (매매 DB kis_quant_db 와 분리)
DEFAULT_OPTUNA_DB_NAME = "kis_optuna"
OPTUNA_STRATEGIES = ("tail", "momentum", "breakout", "scalp")
def mariadb_creds() -> dict:
"""TradeDB(database.py) 와 동일 우선순위 — env > 기본 141."""
return {
"host": os.environ.get("DB_HOST", "192.168.0.141"),
"port": int(os.environ.get("DB_PORT", "3306")),
"user": os.environ.get("DB_USER", "jae"),
"password": os.environ.get("DB_PASS", "1234"),
}
def resolve_optuna_db_name() -> str:
"""
Optuna storage DB — 기본 kis_optuna (매매 kis_quant_db 와 분리).
env OPTUNA_DB_NAME 로 오버라이드 가능.
"""
raw = get_env_from_db("OPTUNA_DB_NAME", "")
if raw and str(raw).strip() not in ("", "None"):
return str(raw).strip()
env = os.environ.get("OPTUNA_DB_NAME", "")
if env and str(env).strip():
return str(env).strip()
return DEFAULT_OPTUNA_DB_NAME
def build_mariadb_storage_url(db_name: Optional[str] = None) -> str:
"""mysql+pymysql://…@141/optuna 형식 storage URL."""
creds = mariadb_creds()
name = (db_name or resolve_optuna_db_name()).strip()
user = quote_plus(creds["user"])
passwd = quote_plus(creds["password"])
return (
f"mysql+pymysql://{user}:{passwd}@{creds['host']}:{creds['port']}/{name}"
f"?charset=utf8mb4"
)
def ensure_optuna_database(db_name: Optional[str] = None) -> str:
"""
MariaDB 141 — kis_optuna 존재 확인 (없으면 CREATE 시도).
"""
name = (db_name or resolve_optuna_db_name()).strip()
creds = mariadb_creds()
try:
import pymysql
except ImportError as exc:
raise ImportError(
"Optuna MariaDB storage 는 pymysql 필요: pip install PyMySQL"
) from exc
# DB 존재 여부만 확인 (이미 있으면 CREATE 생략)
conn = pymysql.connect(
host=creds["host"],
port=creds["port"],
user=creds["user"],
password=creds["password"],
charset="utf8mb4",
autocommit=True,
connect_timeout=10,
)
try:
with conn.cursor() as cur:
cur.execute("SHOW DATABASES LIKE %s", (name,))
exists = cur.fetchone() is not None
if not exists:
cur.execute(
f"CREATE DATABASE IF NOT EXISTS `{name}` "
"DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
)
logger.info(
"📦 Optuna DB 생성: %s@%s:%s/%s",
creds["user"], creds["host"], creds["port"], name,
)
else:
logger.info(
"📦 Optuna storage DB: %s@%s:%s/%s",
creds["user"], creds["host"], creds["port"], name,
)
except Exception as exc:
logger.error("❌ Optuna DB '%s' 접속/확인 실패: %s", name, exc)
raise
finally:
conn.close()
return name
def resolve_optuna_storage_url(cli_override: Optional[str] = None) -> str:
"""
Storage URL 우선순위:
1) CLI --storage
2) OPTUNA_STORAGE_URL (DB/env)
3) MariaDB 141 / kis_optuna (TradeDB 동일 계정)
"""
if cli_override and str(cli_override).strip():
return str(cli_override).strip()
from_db = get_env_from_db("OPTUNA_STORAGE_URL", "")
if from_db and str(from_db).strip() not in ("", "None"):
return str(from_db).strip()
db_name = ensure_optuna_database()
return build_mariadb_storage_url(db_name)
def resolve_study_name(
*,
strategy: str,
mode: str,
start: str,
end: str,
cli_override: Optional[str] = None,
) -> str:
"""Study 이름 — 전략·기간·모드 포함."""
if cli_override and str(cli_override).strip():
return str(cli_override).strip()
env_key = f"OPTUNA_{strategy.upper()}_STUDY_NAME"
from_db = get_env_from_db(env_key, "")
if from_db and str(from_db).strip() not in ("", "None"):
return str(from_db).strip()
legacy = get_env_from_db("OPTUNA_TAIL_STUDY_NAME", "")
if strategy == "tail" and legacy and str(legacy).strip() not in ("", "None"):
return str(legacy).strip()
return f"{strategy}_{mode}_{start}_{end}"
def optuna_run_lock_name(strategy: str) -> str:
return f"{strategy}_param_search_optuna"
def release_shared_tick_store(ctx: Any, *, log: Optional[logging.Logger] = None) -> None:
"""
Optuna ctx.shared_tick_store 해제.
주의: ticks_by_code 가 공유메모리 뷰인 경우, unlink 이후 접근하면
SIGBUS/강제종료(트레이스백 없음) 난다. 최빈(mode_combo) 실측·JSON 저장이
끝난 뒤에만 호출할 것. optimize() 직후 즉시 unlink 금지.
"""
lg = log or logger
store = getattr(ctx, "shared_tick_store", None)
if store is None:
return
try:
store.unlink()
except Exception as exc:
lg.warning("⚠️ shared_tick_store unlink 실패: %s", exc)
try:
ctx.shared_tick_store = None
except Exception:
pass
def announce_optuna_json_path(
out_path: str,
*,
strategy: str = "",
mode: str = "",
note: str = "",
log: Optional[logging.Logger] = None,
) -> str:
"""
결과 JSON 절대경로를 터미널·로그에 눈에 띄게 고지.
또한 logs/optuna_<strategy>_<mode>_latest.jsonpath 에 기록 (없으면 strategy만).
"""
abs_path = os.path.abspath(str(out_path or "").strip())
lg = log or logger
tag = note.strip() or "결과 JSON"
line = f"📁 [{tag}] {abs_path}"
# logger + print 이중 — nohup 로그·터미널 모두에서 바로 보이게
lg.info("%s", line)
print(line, flush=True)
print(f"OPTUNA_RESULT_JSON={abs_path}", flush=True)
try:
root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
logs_dir = os.path.join(root, "logs")
os.makedirs(logs_dir, exist_ok=True)
s = (strategy or "optuna").strip().lower() or "optuna"
m = (mode or "run").strip().lower() or "run"
for name in (
f"optuna_{s}_{m}_latest.jsonpath",
f"optuna_{s}_latest.jsonpath",
"optuna_latest.jsonpath",
):
with open(os.path.join(logs_dir, name), "w", encoding="utf-8") as f:
f.write(abs_path + "\n")
except OSError as exc:
lg.warning("⚠️ jsonpath 사이드카 기록 실패: %s", exc)
return abs_path