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.
370 lines
13 KiB
Python
370 lines
13 KiB
Python
"""
|
||
옵투나·백테 피드 추적 로그 — 실매 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"
|