feat: Enhance Optuna integration and logging for backtesting framework

Changes:
- Added new API endpoints for continuing and confirming Optuna jobs, allowing for better management of ongoing studies.
- Introduced detailed logging for tick feed tracking and order book processing, improving traceability of vendor performance during backtests.
- Updated database schema to include new fields for managing Optuna study results, enhancing the ability to track study progress and outcomes.
- Refactored existing functions to utilize the new logging and tracking features, ensuring consistency across the backtesting framework.

Impact:
- These enhancements improve the robustness and transparency of the Optuna backtesting process, facilitating better analysis and optimization of trading strategies.
This commit is contained in:
Your Name
2026-08-21 19:05:23 +09:00
parent 0ecac7cb95
commit 0780b2cdd0
76 changed files with 4648 additions and 516 deletions

View File

@@ -0,0 +1,369 @@
"""
옵투나·백테 피드 추적 로그 — 실매 TRIGGER_FEED_DETAIL 과 같은 축.
무엇을 보나:
- 틱/호가 DB 로드 후 벤더(kis|kiwoom|ls|…) 건수·비율
- 설정 1·2·3차 체인 (실매 live_*_primary 와 동일)
- (옵션) 매수 후보 샘플: px·틱타임·호가 bid/ask·entry_source
실매 `_scan_log` 꼬리와 문구를 맞춰, 옵투나 로그만 봐도 “어느 벤더로 체결·호가판정했는지” 추적 가능.
"""
from __future__ import annotations
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple
from kis_trader.utils.env import get_env_bool, get_env_int
from kis_trader.utils.logger import get_logger
logger = get_logger("kis_trader.optuna_feed_trace")
# 프로세스당 샘플 상한 (옵투나 trial×종목 폭주 방지)
_sample_logged = 0
_pp_sample_logged = 0
def bt_feed_detail_enabled() -> bool:
"""옵투나 TRIGGER 샘플 ON/OFF (벤더 비율 INFO는 항상)."""
return get_env_bool("BT_FEED_DETAIL_LOG", True)
def reset_bt_feed_sample_counter(*, postprocess: bool = False) -> None:
"""후처리 시작 시 샘플 카운터 리셋 — 코어 TPE에서 소진돼도 호가방 로그가 나오게."""
global _sample_logged, _pp_sample_logged
if postprocess:
_pp_sample_logged = 0
else:
_sample_logged = 0
def log_bt_postprocess_ob_db_scope(
db: Any,
*,
table: str,
cols: Optional[List[str]] = None,
source_filter: Optional[tuple] = None,
date_from: str = "",
date_to: str = "",
context: str = "호가후처리",
) -> None:
"""후처리 호가 추천 직전 — 테이블·기간·source 건수 (코어 TPE 호가OFF 와 무관)."""
try:
from kis_trader.engine.feed_fallback import live_ob_primary
primary = live_ob_primary()
except Exception:
primary = "?"
sf = tuple(source_filter or ())
logger.info(
"🔎 [%s] table=%s | 1차설정=%s | source_filter=%s | 기간=%s~%s",
context, table, primary, sf or "(all)",
(date_from or "?")[:10], (date_to or "?")[:10],
)
colset = set(cols or [])
if not db or "source" not in colset:
if db and "source" not in colset:
logger.info("🔎 [%s] source 컬럼 없음 — 벤더 비율 집계 스킵", context)
return
try:
where: List[str] = []
params: List[Any] = []
df = str(date_from or "").replace("-", "").strip()[:8]
dt = str(date_to or "").replace("-", "").strip()[:8]
if len(df) == 8:
where.append("snap_time >= %s")
params.append(df + "000000")
if len(dt) == 8:
where.append("snap_time <= %s")
params.append(dt + "235959")
if sf:
ph = ",".join(["%s"] * len(sf))
where.append(f"source IN ({ph})")
params.extend(sf)
wh = (" WHERE " + " AND ".join(where)) if where else ""
sql = f"SELECT source, COUNT(*) AS n FROM {table}{wh} GROUP BY source ORDER BY n DESC"
rows = db.conn.execute(sql, tuple(params)).fetchall()
ctr: Counter = Counter()
for r in rows or []:
k = str((r.get("source") if isinstance(r, dict) else r[0]) or "?").strip().lower() or "?"
n = int((r.get("n") if isinstance(r, dict) else r[1]) or 0)
ctr[k] += n
logger.info(
"🔎 [%s DB벤더] %s",
context, _fmt_counter(ctr) if ctr else "(0건 — 기간/필터 미스)",
)
except Exception as e:
logger.warning("⚠️ [%s] DB 벤더 집계 실패: %s", context, e)
def maybe_log_bt_ob_postprocess_sample(
*,
code: str,
buy_time: str,
buy_price: float,
snap_t: str = "",
best_bid: int = 0,
best_ask: int = 0,
spread_pct: float = 0.0,
bid_ask_ratio: float = 0.0,
snap_source: str = "",
hit: bool = True,
context: str = "호가후처리샘플",
) -> None:
"""후처리: 체결↔호가 스냅 매칭 샘플 (상한 BT_FEED_DETAIL_LOG_MAX)."""
global _pp_sample_logged
if not bt_feed_detail_enabled():
return
max_n = max(0, get_env_int("BT_FEED_DETAIL_LOG_MAX", 40))
if max_n <= 0 or _pp_sample_logged >= max_n:
return
try:
from kis_trader.engine.feed_fallback import live_ob_primary
op = live_ob_primary()
alt = "kiwoom" if op == "kis" else "kis"
src = str(snap_source or "").strip().lower() or ("없음" if not hit else "?")
if hit:
logger.info(
"🔎 [%s] %s buy=%s px=%s | 호가1차설정=%s(체인 %s%s→ls) | "
"호가실제 src=%s snap=%s bid=%s ask=%s spr=%.3f%% or=%.3f",
context, code, buy_time, int(buy_price or 0),
op, op, alt,
src, snap_t or "-",
int(best_bid or 0), int(best_ask or 0),
float(spread_pct or 0), float(bid_ask_ratio or 0),
)
else:
logger.info(
"🔎 [%s] %s buy=%s px=%s | 호가1차설정=%s | 호가실제=없음(스냅 미스)",
context, code, buy_time, int(buy_price or 0), op,
)
_pp_sample_logged += 1
except Exception as e:
logger.debug("호가후처리샘플 스킵: %s", e)
def log_bt_feed_chain_banner(*, context: str = "Optuna/백테") -> None:
"""로드 직전 — 설정상 1·2·3차 체인만 (실매 format_trigger_feed_trace 머리와 동일)."""
try:
from kis_trader.engine.feed_fallback import live_ob_primary, live_tick_primary
tp = live_tick_primary()
op = live_ob_primary()
alt_t = "kiwoom" if tp == "kis" else "kis"
alt_o = "kiwoom" if op == "kis" else "kis"
tick_src_env = ""
try:
import os
tick_src_env = str(os.environ.get("TICK_SOURCE") or "").strip().lower()
except Exception:
pass
extra = f" | TICK_SOURCE강제={tick_src_env}" if tick_src_env else ""
logger.info(
"🔎 [%s 피드체인] 틱1차=%s(체인 %s%s→ls) | 호가1차=%s(체인 %s%s→ls)%s",
context, tp, tp, alt_t, op, op, alt_o, extra,
)
except Exception as e:
logger.debug("피드체인 배너 스킵: %s", e)
def _pct(n: int, total: int) -> float:
return (100.0 * n / total) if total > 0 else 0.0
def _fmt_counter(ctr: Counter, *, total: Optional[int] = None) -> str:
tot = int(total if total is not None else sum(ctr.values()))
if tot <= 0:
return "(0건)"
parts = [
f"{k or '?'}={v:,}({_pct(v, tot):.1f}%)"
for k, v in ctr.most_common()
]
return f"total={tot:,} | " + " ".join(parts)
def count_tick_sources(
ticks_by_code: Optional[Dict[str, Any]],
) -> Counter:
"""ticks_by_code {code: {minute: [tick|TickColumnView]}} → source Counter."""
ctr: Counter = Counter()
if not ticks_by_code:
return ctr
try:
from kis_trader.backtest.shared_ticks import TickColumnView
except Exception:
TickColumnView = () # type: ignore
for _code, minutes in ticks_by_code.items():
if not isinstance(minutes, dict):
continue
for _mk, ticks in minutes.items():
if TickColumnView and isinstance(ticks, TickColumnView):
owner = ticks.owner
src_arr = getattr(owner, "_source", None)
if src_arr is None:
ctr["?"] += len(list(ticks.iter_idx()))
continue
for i in ticks.iter_idx():
try:
s = src_arr[i].decode("utf-8").strip().lower() or "?"
except Exception:
s = "?"
ctr[s] += 1
continue
for t in (ticks or []):
if not isinstance(t, dict):
ctr["?"] += 1
continue
s = str(t.get("source") or "").strip().lower() or "?"
ctr[s] += 1
return ctr
def count_orderbook_sources(
orderbook_by_code: Optional[Dict[str, Any]],
) -> Counter:
"""orderbook_by_code {code: {minute: [OrderbookSnapshot|dict]}}."""
ctr: Counter = Counter()
if not orderbook_by_code:
return ctr
for _code, minutes in orderbook_by_code.items():
if not isinstance(minutes, dict):
continue
for _mk, snaps in minutes.items():
for snap in (snaps or []):
try:
s = str(getattr(snap, "source", None) or "").strip().lower()
if not s and isinstance(snap, dict):
s = str(snap.get("source") or "").strip().lower()
ctr[s or "?"] += 1
except Exception:
ctr["?"] += 1
return ctr
def log_bt_tick_feed_trace(
ticks_by_code: Optional[Dict[str, Any]],
*,
table: str = "ws_ticks",
main_src: str = "",
raw_total: Optional[int] = None,
context: str = "틱로드",
) -> Counter:
"""틱 로드·시간축 폴백 직후 — 벤더 비율 INFO."""
ctr = count_tick_sources(ticks_by_code)
kept = sum(ctr.values())
try:
from kis_trader.engine.feed_fallback import live_tick_primary
primary = str(main_src or live_tick_primary()).strip().lower() or "?"
except Exception:
primary = str(main_src or "?").strip().lower() or "?"
raw_bit = ""
if raw_total is not None and int(raw_total) != kept:
raw_bit = f" | raw={int(raw_total):,}→kept={kept:,}"
logger.info(
"🔎 [%s] table=%s | 1차설정=%s | 벤더 %s%s",
context, table, primary, _fmt_counter(ctr, total=kept), raw_bit,
)
return ctr
def log_bt_orderbook_feed_trace(
orderbook_by_code: Optional[Dict[str, Any]],
meta: Optional[Dict[str, Any]] = None,
*,
context: str = "호가로드",
) -> Counter:
"""호가 스냅 로드 직후 — source(kiwoom_0d|ls_uh1|log_backfill|…) 비율."""
ctr = count_orderbook_sources(orderbook_by_code)
meta = meta or {}
hist = str(meta.get("orderbook_history_source") or "").strip().lower() or "?"
rows = int(meta.get("ws_orderbook_rows_loaded") or sum(ctr.values()) or 0)
try:
from kis_trader.engine.feed_fallback import live_ob_primary
primary = live_ob_primary()
except Exception:
primary = "?"
logger.info(
"🔎 [%s] history_source=%s | 1차설정=%s | rows=%s | 벤더 %s",
context, hist, primary, f"{rows:,}", _fmt_counter(ctr),
)
return ctr
def maybe_log_bt_trigger_sample(
code: str,
eval_params: Optional[Dict[str, Any]],
*,
entry_price: float = 0.0,
entry_time: str = "",
entry_src: str = "",
reject: str = "",
context: str = "TRIGGER샘플",
) -> None:
"""매수 평가 직후 샘플 몇 건 — 실매 format_trigger_feed_trace 꼬리와 동일 축."""
global _sample_logged
if not bt_feed_detail_enabled():
return
max_n = max(0, get_env_int("BT_FEED_DETAIL_LOG_MAX", 40))
if max_n <= 0 or _sample_logged >= max_n:
return
p = eval_params or {}
try:
from kis_trader.engine.feed_fallback import (
extract_ob_trace_fields,
format_trigger_feed_trace,
tick_feed_tier,
)
tick_rec: Dict[str, Any] = {}
if entry_price and entry_src:
vendor = ""
es = str(entry_src or "")
if ":" in es:
vendor = es.split(":", 1)[1].strip().lower()
tick_rec = {
"vendor": vendor or "ws_ticks",
"tier": 1,
"label": es or "ws_ticks",
"price": float(entry_price),
"tick_time": str(entry_time or "")[:14],
}
ob_snap = p.get("_backtest_orderbook_snapshot")
ob_rec: Dict[str, Any] = {}
if ob_snap is not None:
ob_rec = extract_ob_trace_fields(ob_snap)
raw_src = str(
ob_rec.get("ob_source") or getattr(ob_snap, "source", "") or ""
).strip().lower()
# kiwoom_0d / ls_uh1 / kis → 체인 벤더 라벨
vendor = "ls" if raw_src.startswith("ls") else (
"kiwoom" if "kiwoom" in raw_src or raw_src == "log_backfill" else (
"kis" if "kis" in raw_src else (raw_src or "ob")
)
)
ob_rec["vendor"] = vendor
ob_rec["tier"] = tick_feed_tier(vendor, "")
ob_rec["label"] = raw_src or vendor
tail = format_trigger_feed_trace(tick_rec, ob_rec)
rej = f" reject={reject}" if reject else ""
logger.info(
"🔎 [%s] %s entry_src=%s%s | %s",
context, code, entry_src or "-", rej, tail,
)
_sample_logged += 1
except Exception as e:
logger.debug("TRIGGER샘플 스킵: %s", e)
def tick_source_label(tick: Optional[Dict[str, Any]], fallback: str = "ws_ticks") -> str:
"""진입 source 문자열 — ws_ticks:kis / ws_ticks:kiwoom / ohlc_open."""
if not tick:
return fallback
src = str(tick.get("source") or "").strip().lower()
if src:
return f"ws_ticks:{src}"
return "ws_ticks"