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:
@@ -8,6 +8,7 @@ kis_trader/engine/feed_fallback.py — 실매·옵투나 공통 읽기 나이
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db
|
||||
@@ -131,6 +132,177 @@ def format_mm_feed_line(tick_lab: str, ob_lab: str) -> str:
|
||||
return f"시세: {t} | 호가: {o}"
|
||||
|
||||
|
||||
def trigger_feed_detail_log_enabled() -> bool:
|
||||
"""트리거(매수체크) 로그에 틱·호가 상세 추적 줄. 기본 ON."""
|
||||
try:
|
||||
return bool(get_env_bool("TRIGGER_FEED_DETAIL_LOG", True))
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def extract_tick_trace_fields(data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""get_price dict → 로그용 현재가·틱타임·나이."""
|
||||
out: Dict[str, Any] = {}
|
||||
if not isinstance(data, dict):
|
||||
return out
|
||||
px = (
|
||||
data.get("stck_prpr")
|
||||
or data.get("price")
|
||||
or data.get("cur_prc")
|
||||
or data.get("close")
|
||||
or 0
|
||||
)
|
||||
try:
|
||||
out["price"] = float(str(px).replace(",", "") or 0)
|
||||
except (TypeError, ValueError):
|
||||
out["price"] = 0.0
|
||||
tt = (
|
||||
data.get("tick_time")
|
||||
or data.get("tick_time_raw")
|
||||
or data.get("chetime")
|
||||
or data.get("cntr_tm")
|
||||
or data.get("FID20")
|
||||
or data.get("stck_cntg_hour")
|
||||
or data.get("cntg_hour")
|
||||
or ""
|
||||
)
|
||||
out["tick_time"] = str(tt or "").strip()
|
||||
if not out["tick_time"]:
|
||||
bs = str(data.get("kis_bsop_date_raw") or "").strip()
|
||||
hr = str(
|
||||
data.get("kis_cntg_hour_raw")
|
||||
or data.get("chetime")
|
||||
or data.get("kiwoom_fid20")
|
||||
or ""
|
||||
).strip()
|
||||
if bs and hr:
|
||||
out["tick_time"] = (bs + hr)[:14]
|
||||
elif hr:
|
||||
out["tick_time"] = hr
|
||||
# 키움 캐시에 tick_time 키가 없어도 chetime만으로 lag 계산
|
||||
if data.get("chetime") and not out.get("tick_time"):
|
||||
out["tick_time"] = str(data.get("chetime") or "").strip()
|
||||
if data.get("_age_ms") is not None:
|
||||
try:
|
||||
out["age_ms"] = int(data.get("_age_ms") or 0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
lag = packet_lag_seconds(out.get("tick_time") or "")
|
||||
if lag is not None:
|
||||
out["lag_sec"] = round(float(lag), 2)
|
||||
if data.get("_feed_vendor"):
|
||||
out["vendor"] = str(data.get("_feed_vendor") or "").strip().lower()
|
||||
return out
|
||||
|
||||
|
||||
def extract_ob_trace_fields(snap: Any) -> Dict[str, Any]:
|
||||
"""호가 스냅샷 → bid/ask/스프레드/잔량비·스냅시각."""
|
||||
out: Dict[str, Any] = {}
|
||||
if snap is None:
|
||||
return out
|
||||
try:
|
||||
if hasattr(snap, "best_bid"):
|
||||
out["best_bid"] = int(snap.best_bid() or 0)
|
||||
out["best_ask"] = int(snap.best_ask() or 0)
|
||||
try:
|
||||
out["spread_pct"] = round(float(snap.spread_pct() or 0), 3)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
bq = int(getattr(snap, "total_bid_qty", 0) or 0)
|
||||
aq = int(getattr(snap, "total_ask_qty", 0) or 0)
|
||||
out["bid_qty"] = bq
|
||||
out["ask_qty"] = aq
|
||||
if aq > 0:
|
||||
out["bid_ask_ratio"] = round(bq / float(aq), 3)
|
||||
except Exception:
|
||||
pass
|
||||
out["snap_time"] = str(getattr(snap, "snap_time", "") or "").strip()
|
||||
src = str(getattr(snap, "source", "") or "").strip()
|
||||
if src:
|
||||
out["ob_source"] = src
|
||||
if getattr(snap, "ts", None):
|
||||
try:
|
||||
out["age_ms"] = int(max(0.0, (time.time() - float(snap.ts)) * 1000))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(snap, dict):
|
||||
try:
|
||||
out["best_bid"] = int(float(snap.get("best_bid") or snap.get("bidp1") or 0))
|
||||
out["best_ask"] = int(float(snap.get("best_ask") or snap.get("askp1") or 0))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
out["snap_time"] = str(snap.get("snap_time") or "").strip()
|
||||
return out
|
||||
|
||||
|
||||
def format_trigger_feed_trace(
|
||||
tick_rec: Optional[Dict[str, Any]],
|
||||
ob_rec: Optional[Dict[str, Any]],
|
||||
*,
|
||||
tick_primary: str = "",
|
||||
ob_primary: str = "",
|
||||
) -> str:
|
||||
"""매수체크 로그 꼬리 — 벤더(1·2·3차) + 현재가 + 틱타임 + 호가."""
|
||||
tp = str(tick_primary or live_tick_primary()).strip().lower() or "kiwoom"
|
||||
op = str(ob_primary or live_ob_primary()).strip().lower() or "kiwoom"
|
||||
alt_t = "kiwoom" if tp == "kis" else "kis"
|
||||
alt_o = "kiwoom" if op == "kis" else "kis"
|
||||
parts: List[str] = [
|
||||
f"틱1차설정={tp}(체인 {tp}→{alt_t}→ls)",
|
||||
f"호가1차설정={op}(체인 {op}→{alt_o}→ls)",
|
||||
]
|
||||
tr = tick_rec or {}
|
||||
if tr.get("label") or tr.get("vendor"):
|
||||
lab = str(tr.get("label") or format_vendor_label(
|
||||
str(tr.get("vendor") or ""),
|
||||
int(tr.get("tier") or tick_feed_tier(str(tr.get("vendor") or ""), tp)),
|
||||
bool(tr.get("spilled")),
|
||||
))
|
||||
bit = [f"틱실제={lab}"]
|
||||
if tr.get("price"):
|
||||
try:
|
||||
bit.append("px=%s" % int(float(tr.get("price") or 0)))
|
||||
except (TypeError, ValueError):
|
||||
bit.append("px=%s" % tr.get("price"))
|
||||
if tr.get("tick_time"):
|
||||
bit.append("t=%s" % tr.get("tick_time"))
|
||||
if tr.get("lag_sec") is not None:
|
||||
bit.append("lag=%ss" % tr.get("lag_sec"))
|
||||
elif tr.get("age_ms") is not None:
|
||||
bit.append("age=%sms" % tr.get("age_ms"))
|
||||
parts.append(" ".join(bit))
|
||||
else:
|
||||
parts.append("틱실제=없음")
|
||||
obr = ob_rec or {}
|
||||
if obr.get("label") or obr.get("vendor"):
|
||||
lab = str(obr.get("label") or format_vendor_label(
|
||||
str(obr.get("vendor") or ""),
|
||||
int(obr.get("tier") or tick_feed_tier(str(obr.get("vendor") or ""), op)),
|
||||
bool(obr.get("spilled")),
|
||||
))
|
||||
bit = [f"호가실제={lab}"]
|
||||
if obr.get("best_bid") or obr.get("best_ask"):
|
||||
bit.append("bid=%s ask=%s" % (obr.get("best_bid") or 0, obr.get("best_ask") or 0))
|
||||
if obr.get("spread_pct") is not None:
|
||||
bit.append("spr=%s%%" % obr.get("spread_pct"))
|
||||
if obr.get("bid_ask_ratio") is not None:
|
||||
bit.append("or=%s" % obr.get("bid_ask_ratio"))
|
||||
if obr.get("snap_time"):
|
||||
bit.append("snap=%s" % obr.get("snap_time"))
|
||||
if obr.get("ob_source"):
|
||||
bit.append("src=%s" % obr.get("ob_source"))
|
||||
if obr.get("age_ms") is not None:
|
||||
bit.append("age=%sms" % obr.get("age_ms"))
|
||||
parts.append(" ".join(bit))
|
||||
else:
|
||||
parts.append("호가실제=없음")
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
def _tick_second_key(tick: Dict[str, Any]) -> str:
|
||||
tt = str(tick.get("tick_time") or "")[:14]
|
||||
if len(tt) >= 14:
|
||||
|
||||
Reference in New Issue
Block a user