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:
@@ -7,6 +7,9 @@ CANDLE_SOURCE=kis|kiwoom 이면 그 메인(+구멍). 빈값이면 LIVE_TICK_PROV
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from kis_trader.ws.candle_series import (
|
||||
@@ -18,6 +21,8 @@ from kis_trader.ws.candle_series import (
|
||||
|
||||
ReadPair = Tuple[str, str]
|
||||
|
||||
logger = logging.getLogger("bt_candle_source")
|
||||
|
||||
# 레거시 호환 이름 (실제 필터는 live_read_pairs)
|
||||
BT_WS_CANDLE_SOURCES: Tuple[str, ...] = ("kiwoom", "kis", "rest", "rollup_1m")
|
||||
|
||||
@@ -75,28 +80,116 @@ def _pairs_in_sql(pairs: Sequence[ReadPair]) -> Tuple[str, List[str]]:
|
||||
return " AND (" + " OR ".join(parts) + ")", params
|
||||
|
||||
|
||||
def _ticks_for_bar_garbage(db, code: str, start_key: str, end_key: str) -> Optional[List[Dict[str, Any]]]:
|
||||
from kis_trader.engine.feed_fallback import candle_garbage_fallback_enabled
|
||||
|
||||
if not candle_garbage_fallback_enabled():
|
||||
return None
|
||||
def _tick_time_bounds(start_key: str, end_key: str) -> Tuple[str, str]:
|
||||
sk = str(start_key or "").strip()
|
||||
ek = str(end_key or "").strip()
|
||||
if len(sk) == 12:
|
||||
sk = sk + "00"
|
||||
if len(ek) == 12:
|
||||
ek = ek + "59"
|
||||
return sk, ek
|
||||
|
||||
|
||||
def _ws_ticks_table(market: Optional[str]) -> str:
|
||||
mk = (market or "").strip().upper()
|
||||
if mk == "US":
|
||||
return "ws_ticks_us"
|
||||
return "ws_ticks"
|
||||
|
||||
|
||||
def _ticks_for_bar_garbage(
|
||||
db,
|
||||
code: str,
|
||||
start_key: str,
|
||||
end_key: str,
|
||||
*,
|
||||
market: Optional[str] = None,
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
from kis_trader.engine.feed_fallback import candle_garbage_fallback_enabled
|
||||
|
||||
if not candle_garbage_fallback_enabled():
|
||||
return None
|
||||
sk, ek = _tick_time_bounds(start_key, end_key)
|
||||
mk = (market or "").strip().upper()
|
||||
table = _ws_ticks_table(mk if mk in ("US", "KR") else "KR")
|
||||
try:
|
||||
rows = db.conn.execute(
|
||||
"SELECT tick_time, source, tick_time_raw FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time >= %s AND tick_time <= %s",
|
||||
(str(code).strip(), sk, ek),
|
||||
).fetchall()
|
||||
if mk in ("US", "KR"):
|
||||
rows = db.conn.execute(
|
||||
f"SELECT tick_time, source, tick_time_raw FROM {table} "
|
||||
"WHERE market=%s AND code=%s AND tick_time >= %s AND tick_time <= %s",
|
||||
(mk, str(code).strip(), sk, ek),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = db.conn.execute(
|
||||
f"SELECT tick_time, source, tick_time_raw FROM {table} "
|
||||
"WHERE market=%s AND code=%s AND tick_time >= %s AND tick_time <= %s",
|
||||
("KR", str(code).strip(), sk, ek),
|
||||
).fetchall()
|
||||
return [dict(r) for r in (rows or [])]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _load_ticks_by_code_bulk(
|
||||
db,
|
||||
start_key: str,
|
||||
end_key: str,
|
||||
*,
|
||||
market: Optional[str] = None,
|
||||
codes_filter: Optional[Sequence[str]] = None,
|
||||
) -> Optional[Dict[str, List[Dict[str, Any]]]]:
|
||||
"""쓰레기 검사용 틱 — 기간 1~2쿼리. OFF 면 None (봉만 dedupe)."""
|
||||
from kis_trader.engine.feed_fallback import candle_garbage_fallback_enabled
|
||||
|
||||
if not candle_garbage_fallback_enabled():
|
||||
return None
|
||||
sk, ek = _tick_time_bounds(start_key, end_key)
|
||||
mk = (market or "").strip().upper()
|
||||
out: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||
want: List[str] = []
|
||||
if codes_filter:
|
||||
want = [str(c).strip() for c in codes_filter if str(c).strip()]
|
||||
in_sql = ""
|
||||
in_params: List[str] = []
|
||||
if want:
|
||||
in_sql = " AND code IN (" + ",".join(["%s"] * len(want)) + ")"
|
||||
in_params = want
|
||||
|
||||
def _pull(table: str, mkt: str) -> int:
|
||||
rows = db.conn.execute(
|
||||
f"SELECT code, tick_time, source, tick_time_raw FROM {table} "
|
||||
"WHERE market=%s AND tick_time >= %s AND tick_time <= %s"
|
||||
+ in_sql,
|
||||
(mkt, sk, ek, *in_params),
|
||||
).fetchall()
|
||||
n = 0
|
||||
for r in rows or []:
|
||||
code = str(r.get("code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
out[code].append(dict(r))
|
||||
n += 1
|
||||
return n
|
||||
|
||||
try:
|
||||
n_kr = n_us = 0
|
||||
if mk == "US":
|
||||
n_us = _pull("ws_ticks_us", "US")
|
||||
elif mk == "KR":
|
||||
n_kr = _pull("ws_ticks", "KR")
|
||||
else:
|
||||
n_kr = _pull("ws_ticks", "KR")
|
||||
try:
|
||||
n_us = _pull("ws_ticks_us", "US")
|
||||
except Exception:
|
||||
n_us = 0
|
||||
logger.info("📥 틱 bulk 쓰레기검사용: KR=%s US=%s 종목=%s", n_kr, n_us, len(out))
|
||||
return dict(out)
|
||||
except Exception as exc:
|
||||
logger.warning("틱 bulk 스킵(봉만): %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def list_ws_candle_codes(
|
||||
db,
|
||||
timeframe: int,
|
||||
@@ -169,13 +262,136 @@ def fetch_ws_candles_for_code(
|
||||
+ " ORDER BY candle_time ASC",
|
||||
[int(timeframe), code, start_key, end_key, *src_params],
|
||||
).fetchall()
|
||||
ticks = _ticks_for_bar_garbage(db, str(code), start_key, end_key)
|
||||
ticks = _ticks_for_bar_garbage(
|
||||
db, str(code), start_key, end_key, market=mk or None,
|
||||
)
|
||||
return dedupe_by_read_pairs(
|
||||
[dict(r) for r in rows], pairs, ticks=ticks, tf_min=int(timeframe),
|
||||
missing_policy="hole", live_cover=False,
|
||||
)
|
||||
|
||||
|
||||
def fetch_ws_candles_by_code_bulk(
|
||||
db,
|
||||
timeframe: int,
|
||||
start_key: str,
|
||||
end_key: str,
|
||||
*,
|
||||
extra_select: str = "",
|
||||
peak_sel: str = "",
|
||||
market: Optional[str] = None,
|
||||
confirmed_only: bool = True,
|
||||
codes_filter: Optional[Sequence[str]] = None,
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""기간 전체 봉 1쿼리 + 틱 1~2쿼리 후 종목별 기존 쓰레기 검사.
|
||||
|
||||
웜업용 ``fetch_ws_candles_for_code`` / ``fetch_ws_candles_warmup_before`` 는 그대로.
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
pairs = resolve_bt_read_pairs()
|
||||
confirmed_sql = " AND is_confirmed=1" if confirmed_only else ""
|
||||
mk = (market or "").strip().upper()
|
||||
src_sql, src_params = _pairs_in_sql(pairs)
|
||||
cols = (
|
||||
f"code, candle_time, open, high, low, close, volume, source, channel"
|
||||
f"{peak_sel}{extra_select}"
|
||||
)
|
||||
want: List[str] = []
|
||||
if codes_filter:
|
||||
want = [
|
||||
str(c).strip()
|
||||
for c in codes_filter
|
||||
if str(c).strip()
|
||||
]
|
||||
in_sql = ""
|
||||
in_params: List[str] = []
|
||||
if want:
|
||||
in_sql = " AND code IN (" + ",".join(["%s"] * len(want)) + ")"
|
||||
in_params = want
|
||||
|
||||
params: List[Any] = [int(timeframe)]
|
||||
mk_sql = ""
|
||||
if mk:
|
||||
mk_sql = " AND market=%s"
|
||||
params.append(mk)
|
||||
params.extend([start_key, end_key, *src_params, *in_params])
|
||||
|
||||
rows = db.conn.execute(
|
||||
f"SELECT {cols} FROM ws_candles "
|
||||
"WHERE timeframe=%s"
|
||||
+ mk_sql
|
||||
+ " AND candle_time >= %s AND candle_time <= %s"
|
||||
+ confirmed_sql
|
||||
+ src_sql
|
||||
+ in_sql
|
||||
+ " ORDER BY code ASC, candle_time ASC",
|
||||
params,
|
||||
).fetchall()
|
||||
|
||||
by_code: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||
for r in rows or []:
|
||||
code = str(r.get("code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
by_code[code].append(dict(r))
|
||||
|
||||
ticks_map = _load_ticks_by_code_bulk(
|
||||
db, start_key, end_key, market=mk or None, codes_filter=want or None,
|
||||
)
|
||||
out: Dict[str, List[Dict[str, Any]]] = {}
|
||||
stats: Dict[str, Any] = {
|
||||
"slots": 0, "picked": 0, "hole": 0, "garbage_skip": 0,
|
||||
"pick_by": {}, "garbage_by": {}, "raw_by": {},
|
||||
}
|
||||
for code, raw in by_code.items():
|
||||
ticks = None if ticks_map is None else ticks_map.get(code, [])
|
||||
out[code] = dedupe_by_read_pairs(
|
||||
raw, pairs, ticks=ticks, tf_min=int(timeframe),
|
||||
missing_policy="hole", live_cover=False, stats=stats,
|
||||
)
|
||||
elapsed = time.perf_counter() - t0
|
||||
n_tick_rows = 0
|
||||
if ticks_map:
|
||||
n_tick_rows = sum(len(v) for v in ticks_map.values())
|
||||
slots = int(stats.get("slots") or 0) or 1
|
||||
pick_by = stats.get("pick_by") or {}
|
||||
garb_by = stats.get("garbage_by") or {}
|
||||
raw_by = stats.get("raw_by") or {}
|
||||
pick_txt = " ".join(
|
||||
"%s=%s(%.0f%%)" % (k, v, 100.0 * int(v) / max(1, int(stats.get("picked") or 1)))
|
||||
for k, v in sorted(pick_by.items(), key=lambda x: -int(x[1]))
|
||||
) or "—"
|
||||
garb_txt = " ".join(
|
||||
"%s=%s" % (k, v) for k, v in sorted(garb_by.items(), key=lambda x: -int(x[1]))
|
||||
) or "0"
|
||||
raw_txt = " ".join(
|
||||
"%s=%s" % (k, v) for k, v in sorted(raw_by.items(), key=lambda x: -int(x[1]))
|
||||
) or "—"
|
||||
logger.info(
|
||||
"📂 캔들 bulk: 봉 %s행 · 틱 %s행 · 종목 %s · %.2fs (tf=%s market=%s)",
|
||||
len(rows or []),
|
||||
n_tick_rows,
|
||||
len(out),
|
||||
elapsed,
|
||||
int(timeframe),
|
||||
mk or "ALL",
|
||||
)
|
||||
logger.info(
|
||||
"📊 봉 pick 비율(1차→2차→REST): 슬롯 %s · pick %s · hole %s(%.1f%%) · "
|
||||
"쓰레기스킵 %s(슬롯대비 %.1f%%) | raw[%s] | pick[%s] | garbage[%s]",
|
||||
slots,
|
||||
stats.get("picked"),
|
||||
stats.get("hole"),
|
||||
100.0 * int(stats.get("hole") or 0) / slots,
|
||||
stats.get("garbage_skip"),
|
||||
100.0 * int(stats.get("garbage_skip") or 0) / slots,
|
||||
raw_txt,
|
||||
pick_txt,
|
||||
garb_txt,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def fetch_ws_candles_warmup_before(
|
||||
db,
|
||||
code: str,
|
||||
|
||||
Reference in New Issue
Block a user