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:
@@ -28,7 +28,8 @@ def _krw_int(v: Any) -> int:
|
||||
return int(x)
|
||||
|
||||
|
||||
def resolve_post_top_n(default: int = 5) -> int:
|
||||
def resolve_post_top_n(default: int = 10) -> int:
|
||||
"""후처리·웹 Top 표 행 수. 기본 10 (사후/학습/안정 공통)."""
|
||||
return max(1, int(get_env_int("OPTUNA_POST_TOP_N", int(default))))
|
||||
|
||||
|
||||
@@ -95,6 +96,7 @@ def _slim_ob(rec: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"ok": bool(rec.get("ok")),
|
||||
"reason": rec.get("reason") or "",
|
||||
"trade_count": int(rec.get("trade_count") or 0),
|
||||
"fill_count": int(rec.get("fill_count") or 0),
|
||||
"params": dict(rec.get("params") or {}),
|
||||
"orig_stats": _slim_stats(rec.get("orig_stats")),
|
||||
"recommended_stats": _slim_stats(rec.get("recommended_stats")),
|
||||
@@ -337,6 +339,49 @@ def _median_params(params_list: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
return cons
|
||||
|
||||
|
||||
def _ob_8way_attempt_note(pool: List[Dict[str, Any]]) -> Tuple[bool, str]:
|
||||
"""8방 TPE를 돌렸는지(구JSON 아님)와 실패 사유.
|
||||
|
||||
Returns:
|
||||
(attempted, why) — attempted=True 이면 재실행만으로 8방이 안 채워질 수 있음.
|
||||
"""
|
||||
bits: List[str] = []
|
||||
saw_combos = False
|
||||
for a in pool or []:
|
||||
ob = a.get("orderbook") or {}
|
||||
combos = ob.get("combos")
|
||||
if isinstance(combos, dict) and combos:
|
||||
saw_combos = True
|
||||
r = str(ob.get("reason") or "").strip()
|
||||
if r and r not in ("light_skip", "none"):
|
||||
tc = ob.get("trade_count")
|
||||
if r == "not_enough_trades" and tc is not None:
|
||||
bit = "호가스냅%s건(<3)" % int(tc)
|
||||
else:
|
||||
bit = r
|
||||
if bit not in bits:
|
||||
bits.append(bit)
|
||||
return (saw_combos or bool(bits), " · ".join(bits))
|
||||
|
||||
|
||||
def ob_8way_web_hint(topn: Optional[Dict[str, Any]]) -> str:
|
||||
"""웹 상태 한 줄. 8방이 채워졌으면 빈 문자열."""
|
||||
anchors = list((topn or {}).get("postprocess_by_anchor") or [])
|
||||
pool = [a for a in anchors if str(a.get("role") or "") in ("gated", "mode")]
|
||||
if not pool:
|
||||
return "8방 없음"
|
||||
if any(
|
||||
isinstance((a.get("orderbook") or {}).get("combos"), dict)
|
||||
and (a.get("orderbook") or {}).get("combos")
|
||||
for a in pool
|
||||
):
|
||||
return ""
|
||||
attempted, why = _ob_8way_attempt_note(pool)
|
||||
if attempted:
|
||||
return why or "8방 미산출"
|
||||
return "구JSON"
|
||||
|
||||
|
||||
def _anchor_combos_map(ob: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||
"""앵커 orderbook → combo_id dict. 구 JSON은 e/x/s 단독만 복원."""
|
||||
if not isinstance(ob, dict):
|
||||
@@ -400,8 +445,12 @@ def _axis_slice_from_combo(
|
||||
|
||||
|
||||
def _consensus_from_anchors(anchors: List[Dict[str, Any]], strategy: str) -> Dict[str, Any]:
|
||||
"""gated+mode. live·stable 제외. 호가=8방 중 median PnL 최고 방 + median 파라미터."""
|
||||
pool = [a for a in anchors if str(a.get("role") or "") in ("gated", "mode")]
|
||||
"""gated(+learn 폴백)+mode. live·stable 제외. 호가=8방 median PnL 최고 방."""
|
||||
# 사후합격 없으면 학습 Top 후처리(learn)를 합의에 넣음 — WR/PF 때문에 호가방 학습이 비지 않게
|
||||
roles = ("gated", "mode")
|
||||
if not any(str(a.get("role") or "") == "gated" for a in (anchors or [])):
|
||||
roles = ("gated", "learn", "mode")
|
||||
pool = [a for a in anchors if str(a.get("role") or "") in roles]
|
||||
by_combo: Dict[str, List[Dict[str, Any]]] = {cid: [] for cid in _COMBO_IDS}
|
||||
for a in pool:
|
||||
combos = _anchor_combos_map(a.get("orderbook") or {})
|
||||
@@ -429,7 +478,11 @@ def _consensus_from_anchors(anchors: List[Dict[str, Any]], strategy: str) -> Dic
|
||||
exit_c: Dict[str, Any] = {"ok": False, "params": {}, "n": 0}
|
||||
stop_c: Dict[str, Any] = {"ok": False, "params": {}, "n": 0}
|
||||
ob_merged: Dict[str, Any] = {}
|
||||
note = "합의=Top5(+mode) 8방 중 median PnL 최고 방 + median 파라미터. 실매 참고행 제외."
|
||||
note = (
|
||||
"합의=Top5(+mode) 8방 중 median PnL 최고 방 + median 파라미터. 실매 참고행 제외."
|
||||
if any(str(a.get("role") or "") == "gated" for a in (anchors or []))
|
||||
else "합의=학습Top(+mode) — 사후합격(gated) 0건이라 WR/PF 미달 후보로 호가방 학습. 실매 참고행 제외."
|
||||
)
|
||||
|
||||
if best_cid:
|
||||
recs = by_combo[best_cid]
|
||||
@@ -475,7 +528,15 @@ def _consensus_from_anchors(anchors: List[Dict[str, Any]], strategy: str) -> Dic
|
||||
ob_merged = dict(entry.get("params") or {})
|
||||
ob_merged.update(exit_c.get("params") or {})
|
||||
ob_merged.update(stop_c.get("params") or {})
|
||||
note = "합의=구JSON 축분리 median(8방 없음). 후처리 재실행 권장."
|
||||
attempted, why = _ob_8way_attempt_note(pool)
|
||||
if attempted:
|
||||
note = (
|
||||
"합의=8방 유효 방 없음(%s). 축분리 median 폴백. "
|
||||
"호가스냅이 늘지 않으면 재실행해도 동일."
|
||||
% (why or "TPE 실패")
|
||||
)
|
||||
else:
|
||||
note = "합의=구JSON 축분리 median(8방 없음). 후처리 재실행 권장."
|
||||
|
||||
ws_params_list = [
|
||||
dict((a.get("whipsaw") or {}).get("params") or {})
|
||||
@@ -635,6 +696,86 @@ def _reuse_ob_ws(anchors: List[Dict[str, Any]], trial: Any) -> Optional[Dict[str
|
||||
return None
|
||||
|
||||
|
||||
def append_learn_postprocess_anchors(
|
||||
data: Dict[str, Any],
|
||||
anchors: List[Dict[str, Any]],
|
||||
*,
|
||||
strat: str,
|
||||
strat_u: str,
|
||||
top_n: int,
|
||||
do_ob: bool,
|
||||
evaluate_fn: Optional[EvalFn],
|
||||
lg: logging.Logger,
|
||||
ob_n_trials: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
사후합격(gated)이 비었을 때 학습 TopN(results)에도 호가/휩쏘 후처리.
|
||||
|
||||
탐색은 WR/PF=0 이라 −PnL도 TPE에 들어가는데, 후처리만 gated(WR40/PF1)에
|
||||
묶이면 gated=0 일 때 호가 8방이 mode/live만 남고 학습 후보가 통째 제외됨.
|
||||
→ 후처리도 ‘끝난 뒤 후보 고르기’이지 ‘처음부터 제외’가 아니게 learn 폴백.
|
||||
"""
|
||||
if any(str(a.get("role") or "") == "gated" for a in anchors):
|
||||
return
|
||||
if any(str(a.get("role") or "") == "learn" for a in anchors):
|
||||
return
|
||||
learn = list(
|
||||
(data or {}).get("results")
|
||||
or (data or {}).get("results_all")
|
||||
or []
|
||||
)[: max(1, int(top_n or 5))]
|
||||
if not learn:
|
||||
return
|
||||
lg.info(
|
||||
"📌 [후처리] 사후합격 0건 → 학습 Top%d 에 호가/휩쏘 후처리 (WR/PF 사후게이트와 분리)",
|
||||
len(learn),
|
||||
)
|
||||
for i, row in enumerate(learn, start=1):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
combo = _combo_from_row(row)
|
||||
fills: List[Dict[str, Any]] = []
|
||||
if do_ob:
|
||||
from kis_trader.backtest import optuna_post_progress as opp
|
||||
opp.next_unit(f"learn#{i}", "학습Top 호가")
|
||||
if do_ob and evaluate_fn is not None:
|
||||
lg.info("📌 [후처리] learn#%d 백테 재실행 (호가/휩쏘 체결)", i)
|
||||
fills = _replay_fills(evaluate_fn, combo, lg)
|
||||
pnl = row.get("total_pnl")
|
||||
trail = _slim_trail(_trail_for_pnl(strat, pnl))
|
||||
ob = {"ok": False, "reason": "light_skip"}
|
||||
ws = {"ok": False, "reason": "light_skip"}
|
||||
if do_ob:
|
||||
if fills:
|
||||
ob = _ob_for_anchor(
|
||||
strategy=strat_u, out_data=data, fills=fills, live=False,
|
||||
lg=lg, n_trials=ob_n_trials,
|
||||
)
|
||||
ws = _ws_for_anchor(
|
||||
strategy=strat_u, out_data=data, fills=fills, live=False, lg=lg,
|
||||
)
|
||||
elif evaluate_fn is None:
|
||||
ob = {"ok": False, "reason": "no_replay_fills"}
|
||||
ws = {"ok": False, "reason": "no_replay_fills"}
|
||||
else:
|
||||
ob = {"ok": False, "reason": "not_enough_trades", "trade_count": 0}
|
||||
ws = {"ok": False, "reason": "not_enough_trades", "trade_count": 0}
|
||||
anchors.append({
|
||||
"id": f"learn#{i}",
|
||||
"role": "learn",
|
||||
"rank": i,
|
||||
"optuna_trial_number": row.get("optuna_trial_number") or row.get("_trial_number"),
|
||||
"total_pnl": _krw_int(pnl),
|
||||
"total_trades": int(row.get("total_trades") or 0),
|
||||
"win_rate": row.get("win_rate"),
|
||||
"pf": row.get("pf"),
|
||||
"orderbook": ob,
|
||||
"whipsaw": ws,
|
||||
"trail": trail,
|
||||
"note": "학습 Top(사후게이트 미달 폴백) — 호가방 학습용 · DB적용은 gated 우선",
|
||||
})
|
||||
|
||||
|
||||
def append_stable_postprocess_anchors(
|
||||
data: Dict[str, Any],
|
||||
anchors: List[Dict[str, Any]],
|
||||
@@ -652,7 +793,13 @@ def append_stable_postprocess_anchors(
|
||||
return
|
||||
if any(str(a.get("role") or "") == "stable" for a in anchors):
|
||||
return
|
||||
stable = list((data or {}).get("results_stable") or [])[: max(1, int(top_n or 5))]
|
||||
from kis_trader.backtest.optuna_common import resolve_results_stable
|
||||
stable, _meta = resolve_results_stable(data, top_n=max(1, int(top_n or 5)))
|
||||
# 후처리 중 JSON에 비어 있으면 재구성분 반영 (다음 요약·앵커 일치)
|
||||
if not list((data or {}).get("results_stable") or []) and stable:
|
||||
data["results_stable"] = list(stable)
|
||||
if _meta:
|
||||
data["stable_gates"] = dict(_meta)
|
||||
for i, row in enumerate(stable, start=1):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
@@ -724,7 +871,7 @@ def ensure_stable_postprocess_on_payload(data: Dict[str, Any]) -> None:
|
||||
before = len(anchors)
|
||||
append_stable_postprocess_anchors(
|
||||
data, anchors,
|
||||
strat=strat, strat_u=strat_u, top_n=resolve_post_top_n(5),
|
||||
strat=strat, strat_u=strat_u, top_n=resolve_post_top_n(10),
|
||||
do_ob=False, evaluate_fn=None, lg=logger, ob_n_trials=0,
|
||||
)
|
||||
if len(anchors) != before:
|
||||
@@ -754,7 +901,7 @@ def attach_topn_postprocess(
|
||||
strat_u = "TAIL"
|
||||
elif strat_u in ("SCALPING", "SCALP"):
|
||||
strat_u = "SCALP"
|
||||
top_n = resolve_post_top_n(5)
|
||||
top_n = resolve_post_top_n(10)
|
||||
do_ob = _run_ob_whipsaw_full() if run_ob_whipsaw is None else bool(run_ob_whipsaw)
|
||||
|
||||
gated = list(data.get("results_gated") or [])[:top_n]
|
||||
@@ -762,14 +909,40 @@ def attach_topn_postprocess(
|
||||
|
||||
from kis_trader.backtest import optuna_post_progress as opp
|
||||
|
||||
stable_preview = list((data or {}).get("results_stable") or [])[: max(1, int(top_n or 5))] if _include_stable() else []
|
||||
n_units = len(gated) + (len(stable_preview) if _include_stable() else 0)
|
||||
stable_preview: List[Any] = []
|
||||
if _include_stable():
|
||||
from kis_trader.backtest.optuna_common import resolve_results_stable
|
||||
stable_preview, _sg = resolve_results_stable(data, top_n=max(1, int(top_n or 5)))
|
||||
if not list((data or {}).get("results_stable") or []) and stable_preview:
|
||||
data["results_stable"] = list(stable_preview)
|
||||
if _sg:
|
||||
data["stable_gates"] = dict(_sg)
|
||||
learn_preview: List[Any] = []
|
||||
if not gated:
|
||||
learn_preview = list(
|
||||
(data or {}).get("results") or (data or {}).get("results_all") or []
|
||||
)[: max(1, int(top_n or 5))]
|
||||
n_units = len(gated) + len(learn_preview) + (len(stable_preview) if _include_stable() else 0)
|
||||
if _include_mode():
|
||||
n_units += 1
|
||||
if _include_live():
|
||||
n_units += 1
|
||||
if do_ob:
|
||||
opp.begin_job(lg, max(1, n_units))
|
||||
try:
|
||||
from kis_trader.backtest.optuna_feed_trace import (
|
||||
log_bt_feed_chain_banner,
|
||||
reset_bt_feed_sample_counter,
|
||||
)
|
||||
|
||||
reset_bt_feed_sample_counter(postprocess=True)
|
||||
log_bt_feed_chain_banner(context="Optuna후처리")
|
||||
lg.info(
|
||||
"🔎 [Optuna후처리] 호가 추천은 코어 TPE(필터OFF)와 별도 — "
|
||||
"아래 [호가후처리]/[호가후처리샘플] 줄을 보면 됨"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for i, row in enumerate(gated, start=1):
|
||||
combo = _combo_from_row(row)
|
||||
@@ -808,6 +981,13 @@ def attach_topn_postprocess(
|
||||
"note": "사후합격 후보",
|
||||
})
|
||||
|
||||
# gated 비면 학습 Top 에도 호가 후처리 (탐색 WR/PF=0 과 같은 정신)
|
||||
append_learn_postprocess_anchors(
|
||||
data, anchors,
|
||||
strat=strat, strat_u=strat_u, top_n=top_n,
|
||||
do_ob=do_ob, evaluate_fn=evaluate_fn, lg=lg, ob_n_trials=ob_n_trials,
|
||||
)
|
||||
|
||||
append_stable_postprocess_anchors(
|
||||
data, anchors,
|
||||
strat=strat, strat_u=strat_u, top_n=top_n,
|
||||
|
||||
Reference in New Issue
Block a user