ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -9,16 +9,467 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from kis_trader.utils.env import get_env_from_db
|
||||
from kis_trader.utils.env import get_env_float, get_env_from_db, get_env_int
|
||||
|
||||
logger = logging.getLogger("optuna_common")
|
||||
|
||||
# Optuna 전용 MariaDB (매매 DB kis_quant_db 와 분리)
|
||||
DEFAULT_OPTUNA_DB_NAME = "kis_optuna"
|
||||
OPTUNA_STRATEGIES = ("tail", "momentum", "breakout", "scalp", "dart")
|
||||
OPTUNA_STRATEGIES = ("tail", "momentum", "us_momentum", "breakout", "scalp", "dart")
|
||||
|
||||
# 탐색(TPE 학습): 게이트 OFF(0) — PnL 차이를 샘플러가 보도록.
|
||||
# 리포트/apply 후보: 아래 REPORT_* 로 사후 필터.
|
||||
OPTUNA_SEARCH_MIN_WIN_RATE_DEFAULT = 0.0
|
||||
OPTUNA_SEARCH_MIN_PF_DEFAULT = 0.0
|
||||
OPTUNA_SEARCH_MIN_TRADES_DEFAULT = 1
|
||||
OPTUNA_REPORT_MIN_WIN_RATE_DEFAULT = 40.0
|
||||
OPTUNA_REPORT_MIN_PF_DEFAULT = 1.0
|
||||
|
||||
# 일별 손익 안정성 티어 (results_stable) — 학습1위/gated 와 별도 후보
|
||||
# 손실일·최악일·일PnL 분산으로 “합산만 큰” 후보를 걸러낸다.
|
||||
OPTUNA_STABLE_MAX_LOSING_DAYS_DEFAULT = 1
|
||||
OPTUNA_STABLE_MIN_WORST_DAY_PNL_DEFAULT = -30000.0
|
||||
OPTUNA_STABLE_LAMBDA_DEFAULT = 1.0
|
||||
OPTUNA_STABLE_MIN_ACTIVE_DAYS_DEFAULT = 2
|
||||
|
||||
|
||||
def optuna_search_gate_defaults() -> Tuple[float, float, int]:
|
||||
"""탐색 중 objective 게이트 (기본 0/0/1). CLI 미지정 시 사용."""
|
||||
return (
|
||||
float(get_env_float("PARAM_SEARCH_OPTUNA_MIN_WIN_RATE", OPTUNA_SEARCH_MIN_WIN_RATE_DEFAULT)),
|
||||
float(get_env_float("PARAM_SEARCH_OPTUNA_MIN_PF", OPTUNA_SEARCH_MIN_PF_DEFAULT)),
|
||||
int(get_env_int("PARAM_SEARCH_OPTUNA_MIN_TRADES", OPTUNA_SEARCH_MIN_TRADES_DEFAULT)),
|
||||
)
|
||||
|
||||
|
||||
def optuna_report_gate_defaults() -> Tuple[float, float, int]:
|
||||
"""결과 후보·apply 사후 필터 (기본 승률40·PF1.0·min_trades=탐색과 동일)."""
|
||||
_sw, _sp, min_tr = optuna_search_gate_defaults()
|
||||
return (
|
||||
float(get_env_float(
|
||||
"PARAM_SEARCH_OPTUNA_REPORT_MIN_WIN_RATE", OPTUNA_REPORT_MIN_WIN_RATE_DEFAULT,
|
||||
)),
|
||||
float(get_env_float(
|
||||
"PARAM_SEARCH_OPTUNA_REPORT_MIN_PF", OPTUNA_REPORT_MIN_PF_DEFAULT,
|
||||
)),
|
||||
int(get_env_int("PARAM_SEARCH_OPTUNA_REPORT_MIN_TRADES", max(1, min_tr))),
|
||||
)
|
||||
|
||||
|
||||
def _sort_optuna_rows(rows: List[Dict[str, Any]], sort_by: str) -> List[Dict[str, Any]]:
|
||||
sb = (sort_by or "pnl").strip().lower()
|
||||
out = list(rows)
|
||||
|
||||
def _f(r: Dict[str, Any], k: str) -> float:
|
||||
try:
|
||||
return float(r.get(k) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
if sb == "score":
|
||||
out.sort(key=lambda r: (-_f(r, "score"), -_f(r, "total_pnl"), -_f(r, "win_rate")))
|
||||
elif sb == "win_rate":
|
||||
out.sort(key=lambda r: (-_f(r, "win_rate"), -_f(r, "total_pnl")))
|
||||
elif sb in ("stability", "stable"):
|
||||
# 일평균 − λ·표준편차(stability_score) 우선 · 최악일 · 합산 PnL
|
||||
out.sort(
|
||||
key=lambda r: (
|
||||
-_f(r, "stability_score"),
|
||||
-_f(r, "worst_day_pnl"),
|
||||
-_f(r, "total_pnl"),
|
||||
-_f(r, "win_rate"),
|
||||
),
|
||||
)
|
||||
else:
|
||||
out.sort(key=lambda r: (-_f(r, "total_pnl"), -_f(r, "win_rate")))
|
||||
return out
|
||||
|
||||
|
||||
def trade_exit_day_key(trade: Dict[str, Any]) -> str:
|
||||
"""청산 시각 → YYYY-MM-DD (없으면 빈 문자열).
|
||||
|
||||
꼬리 백테는 exit_time, 스캘핑·모멘텀·돌파 포트폴리오 백테는 sell_time 을 씀.
|
||||
sell_time 누락 시 daily_pnl/results_stable 이 전부 비게 됨.
|
||||
"""
|
||||
raw = (
|
||||
trade.get("exit_time")
|
||||
or trade.get("sell_date")
|
||||
or trade.get("sell_time") # scalp/momentum/breakout 포트폴리오
|
||||
or trade.get("exit_ts")
|
||||
or trade.get("exit_at")
|
||||
or ""
|
||||
)
|
||||
s = str(raw).strip()
|
||||
if not s:
|
||||
return ""
|
||||
digits = "".join(ch for ch in s if ch.isdigit())
|
||||
if len(digits) >= 8:
|
||||
return f"{digits[0:4]}-{digits[4:6]}-{digits[6:8]}"
|
||||
if len(s) >= 10 and s[4] == "-" and s[7] == "-":
|
||||
return s[:10]
|
||||
return ""
|
||||
|
||||
|
||||
def compute_daily_stability_metrics(
|
||||
trades: List[Dict[str, Any]],
|
||||
*,
|
||||
stability_lambda: Optional[float] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
거래 리스트 → 일별 PnL·안정성 점수.
|
||||
|
||||
stability_score = mean(일PnL) − λ × std(일PnL)
|
||||
(λ 기본 OPTUNA_STABLE_LAMBDA / get_env)
|
||||
"""
|
||||
from statistics import mean, pstdev
|
||||
|
||||
if stability_lambda is None:
|
||||
_, _, lam, _ = optuna_stable_gate_defaults()
|
||||
stability_lambda = lam
|
||||
try:
|
||||
lam = float(stability_lambda)
|
||||
except (TypeError, ValueError):
|
||||
lam = float(OPTUNA_STABLE_LAMBDA_DEFAULT)
|
||||
|
||||
by_day: Dict[str, float] = {}
|
||||
for t in trades or []:
|
||||
day = trade_exit_day_key(t if isinstance(t, dict) else {})
|
||||
if not day:
|
||||
continue
|
||||
try:
|
||||
pnl = float((t or {}).get("pnl") or (t or {}).get("realized_pnl") or 0)
|
||||
except (TypeError, ValueError):
|
||||
pnl = 0.0
|
||||
by_day[day] = by_day.get(day, 0.0) + pnl
|
||||
|
||||
days_sorted = sorted(by_day.keys())
|
||||
vals = [float(by_day[d]) for d in days_sorted]
|
||||
n_days = len(vals)
|
||||
if n_days <= 0:
|
||||
return {
|
||||
"daily_pnl": {},
|
||||
"n_active_days": 0,
|
||||
"n_losing_days": 0,
|
||||
"worst_day_pnl": 0.0,
|
||||
"best_day_pnl": 0.0,
|
||||
"daily_pnl_mean": 0.0,
|
||||
"daily_pnl_std": 0.0,
|
||||
"stability_score": 0.0,
|
||||
"stability_lambda": lam,
|
||||
}
|
||||
|
||||
n_lose = sum(1 for v in vals if v < 0)
|
||||
worst = min(vals)
|
||||
best = max(vals)
|
||||
avg = float(mean(vals))
|
||||
std = float(pstdev(vals)) if n_days >= 2 else 0.0
|
||||
score = avg - lam * std
|
||||
return {
|
||||
"daily_pnl": {d: round(by_day[d], 2) for d in days_sorted},
|
||||
"n_active_days": n_days,
|
||||
"n_losing_days": int(n_lose),
|
||||
"worst_day_pnl": round(worst, 2),
|
||||
"best_day_pnl": round(best, 2),
|
||||
"daily_pnl_mean": round(avg, 2),
|
||||
"daily_pnl_std": round(std, 2),
|
||||
"stability_score": round(score, 4),
|
||||
"stability_lambda": lam,
|
||||
}
|
||||
|
||||
|
||||
def attach_daily_stability(
|
||||
result: Dict[str, Any],
|
||||
trades: List[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
"""evaluate_* 반환 dict 에 일별 안정성 필드를 붙인다."""
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
result.update(compute_daily_stability_metrics(trades or []))
|
||||
return result
|
||||
|
||||
|
||||
def optuna_stable_gate_defaults() -> Tuple[int, float, float, int]:
|
||||
"""(max_losing_days, min_worst_day_pnl, lambda, min_active_days)."""
|
||||
return (
|
||||
int(get_env_int(
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_MAX_LOSING_DAYS",
|
||||
OPTUNA_STABLE_MAX_LOSING_DAYS_DEFAULT,
|
||||
)),
|
||||
float(get_env_float(
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_MIN_WORST_DAY_PNL",
|
||||
OPTUNA_STABLE_MIN_WORST_DAY_PNL_DEFAULT,
|
||||
)),
|
||||
float(get_env_float(
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_LAMBDA",
|
||||
OPTUNA_STABLE_LAMBDA_DEFAULT,
|
||||
)),
|
||||
int(get_env_int(
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_MIN_ACTIVE_DAYS",
|
||||
OPTUNA_STABLE_MIN_ACTIVE_DAYS_DEFAULT,
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
def row_passes_report_gates(
|
||||
row: Dict[str, Any],
|
||||
*,
|
||||
min_win_rate: float,
|
||||
min_pf: float,
|
||||
min_trades: int,
|
||||
) -> bool:
|
||||
try:
|
||||
wr = float(row.get("win_rate") or 0)
|
||||
pf = float(row.get("pf") or 0)
|
||||
nt = int(row.get("total_trades") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if nt < int(min_trades):
|
||||
return False
|
||||
if wr < float(min_win_rate):
|
||||
return False
|
||||
if pf < float(min_pf):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def row_passes_stable_gates(row: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
일별 안정성 사후 게이트.
|
||||
daily_pnl / n_active_days 가 없으면(구 JSON) 통과 불가 → results_stable 빈 목록.
|
||||
"""
|
||||
if row.get("daily_pnl") is None and row.get("n_active_days") is None:
|
||||
return False
|
||||
max_lose, min_worst, _lam, min_days = optuna_stable_gate_defaults()
|
||||
try:
|
||||
n_days = int(row.get("n_active_days") or 0)
|
||||
n_lose = int(row.get("n_losing_days") or 0)
|
||||
worst = float(row.get("worst_day_pnl") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if n_days < int(min_days):
|
||||
return False
|
||||
if n_lose > int(max_lose):
|
||||
return False
|
||||
if worst < float(min_worst):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def set_optuna_trial_stability_attrs(trial: Any, result: Dict[str, Any]) -> None:
|
||||
"""Optuna trial.user_attrs 에 일별 안정성 스냅샷 저장."""
|
||||
import json as _json
|
||||
|
||||
if not result:
|
||||
return
|
||||
try:
|
||||
trial.set_user_attr("n_active_days", int(result.get("n_active_days") or 0))
|
||||
trial.set_user_attr("n_losing_days", int(result.get("n_losing_days") or 0))
|
||||
trial.set_user_attr("worst_day_pnl", float(result.get("worst_day_pnl") or 0))
|
||||
trial.set_user_attr("best_day_pnl", float(result.get("best_day_pnl") or 0))
|
||||
trial.set_user_attr("daily_pnl_mean", float(result.get("daily_pnl_mean") or 0))
|
||||
trial.set_user_attr("daily_pnl_std", float(result.get("daily_pnl_std") or 0))
|
||||
trial.set_user_attr("stability_score", float(result.get("stability_score") or 0))
|
||||
trial.set_user_attr(
|
||||
"daily_pnl_json",
|
||||
_json.dumps(result.get("daily_pnl") or {}, ensure_ascii=False),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def stability_fields_from_trial_attrs(trial: Any) -> Dict[str, Any]:
|
||||
"""trial.user_attrs → 결과 row 안정성 필드."""
|
||||
import json as _json
|
||||
|
||||
raw = trial.user_attrs.get("daily_pnl_json") or "{}"
|
||||
try:
|
||||
daily = _json.loads(raw) if isinstance(raw, str) else (raw or {})
|
||||
except Exception:
|
||||
daily = {}
|
||||
if trial.user_attrs.get("n_active_days") is None and not daily:
|
||||
return {}
|
||||
return {
|
||||
"daily_pnl": daily if isinstance(daily, dict) else {},
|
||||
"n_active_days": int(trial.user_attrs.get("n_active_days") or 0),
|
||||
"n_losing_days": int(trial.user_attrs.get("n_losing_days") or 0),
|
||||
"worst_day_pnl": float(trial.user_attrs.get("worst_day_pnl") or 0),
|
||||
"best_day_pnl": float(trial.user_attrs.get("best_day_pnl") or 0),
|
||||
"daily_pnl_mean": float(trial.user_attrs.get("daily_pnl_mean") or 0),
|
||||
"daily_pnl_std": float(trial.user_attrs.get("daily_pnl_std") or 0),
|
||||
"stability_score": float(trial.user_attrs.get("stability_score") or 0),
|
||||
}
|
||||
|
||||
|
||||
def build_optuna_result_tiers(
|
||||
rows: List[Dict[str, Any]],
|
||||
*,
|
||||
sort_by: str,
|
||||
top_n: int = 5000,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
탐색 전체 vs 리포트/apply 후보 분리.
|
||||
|
||||
- results_all: 완료·게이트통과(탐색게이트) trial 전부 정렬
|
||||
- results: 하위호환 — 플러스 PnL 우선(없으면 all)
|
||||
- results_gated: 승률·PF 사후 필터 (apply 후보)
|
||||
- results_stable: gated ∩ 일별 안정성 게이트 (들쭉날쭉 완화 후보)
|
||||
"""
|
||||
rep_wr, rep_pf, rep_tr = optuna_report_gate_defaults()
|
||||
max_lose, min_worst, lam, min_days = optuna_stable_gate_defaults()
|
||||
all_sorted = _sort_optuna_rows(rows, sort_by)
|
||||
profitable = [r for r in all_sorted if float(r.get("total_pnl") or 0) > 0]
|
||||
learning = profitable if profitable else all_sorted
|
||||
gated = [
|
||||
r for r in all_sorted
|
||||
if row_passes_report_gates(
|
||||
r, min_win_rate=rep_wr, min_pf=rep_pf, min_trades=rep_tr,
|
||||
)
|
||||
and float(r.get("total_pnl") or 0) > 0
|
||||
]
|
||||
stable_pool = [r for r in gated if row_passes_stable_gates(r)]
|
||||
stable = _sort_optuna_rows(stable_pool, "stability")
|
||||
return {
|
||||
"results_all": all_sorted[:top_n],
|
||||
"results": learning[:top_n],
|
||||
"results_gated": gated[:top_n],
|
||||
"results_stable": stable[:top_n],
|
||||
"report_gates": {
|
||||
"min_win_rate": rep_wr,
|
||||
"min_pf": rep_pf,
|
||||
"min_trades": rep_tr,
|
||||
},
|
||||
"stable_gates": {
|
||||
"max_losing_days": max_lose,
|
||||
"min_worst_day_pnl": min_worst,
|
||||
"stability_lambda": lam,
|
||||
"min_active_days": min_days,
|
||||
"score_note": "stability_score = mean(일PnL) − λ × std(일PnL)",
|
||||
},
|
||||
"search_gates_note": (
|
||||
"탐색 min_win_rate/min_pf 기본 0 — TPE가 PnL 차이를 학습. "
|
||||
"적용·운영 후보는 results_gated(report_gates). "
|
||||
"들쭉날쭉 완화 후보는 results_stable(stable_gates)."
|
||||
),
|
||||
"n_results_all": len(all_sorted),
|
||||
"n_results_learning": len(learning),
|
||||
"n_results_gated": len(gated),
|
||||
"n_results_stable": len(stable),
|
||||
}
|
||||
|
||||
|
||||
def pick_gated_apply_trial(
|
||||
study: Any,
|
||||
*,
|
||||
sort_by: str = "pnl",
|
||||
fail_objective: float = -1e18,
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
--apply-best 용: study.best(탐색 objective)가 아니라
|
||||
report_gates 통과 trial 중 정렬 1위.
|
||||
"""
|
||||
import optuna # noqa: WPS433 — 호출 시에만
|
||||
|
||||
rep_wr, rep_pf, rep_tr = optuna_report_gate_defaults()
|
||||
cand: List[Tuple[Dict[str, Any], Any]] = []
|
||||
for trial in study.trials:
|
||||
if trial.state != optuna.trial.TrialState.COMPLETE:
|
||||
continue
|
||||
if not trial.user_attrs.get("gates_ok"):
|
||||
continue
|
||||
try:
|
||||
val = float(trial.value) if trial.value is not None else fail_objective
|
||||
except (TypeError, ValueError):
|
||||
val = fail_objective
|
||||
if val <= fail_objective + 1:
|
||||
continue
|
||||
row = {
|
||||
"win_rate": float(trial.user_attrs.get("win_rate") or 0),
|
||||
"pf": float(trial.user_attrs.get("pf") or 0),
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
"total_pnl": float(trial.user_attrs.get("total_pnl") or 0),
|
||||
"score": float(trial.user_attrs.get("score") or 0),
|
||||
"_trial_number": int(trial.number),
|
||||
}
|
||||
if not row_passes_report_gates(
|
||||
row, min_win_rate=rep_wr, min_pf=rep_pf, min_trades=rep_tr,
|
||||
):
|
||||
continue
|
||||
if float(row["total_pnl"]) <= 0:
|
||||
continue
|
||||
cand.append((row, trial))
|
||||
if not cand:
|
||||
return None
|
||||
ranked = _sort_optuna_rows([r for r, _ in cand], sort_by)
|
||||
top_n = int(ranked[0].get("_trial_number") or -1)
|
||||
for r, t in cand:
|
||||
if int(r.get("_trial_number") or -2) == top_n:
|
||||
return t
|
||||
return cand[0][1]
|
||||
|
||||
|
||||
def ensure_optuna_gate_env_defaults(db: Any = None) -> None:
|
||||
"""신규 Optuna 게이트 키가 DB에 없으면 env_config_ext 에만 UPSERT (전체 스냅샷 X)."""
|
||||
defaults = {
|
||||
"PARAM_SEARCH_OPTUNA_MIN_WIN_RATE": str(OPTUNA_SEARCH_MIN_WIN_RATE_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_MIN_PF": str(OPTUNA_SEARCH_MIN_PF_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_MIN_TRADES": str(OPTUNA_SEARCH_MIN_TRADES_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_REPORT_MIN_WIN_RATE": str(OPTUNA_REPORT_MIN_WIN_RATE_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_REPORT_MIN_PF": str(OPTUNA_REPORT_MIN_PF_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_BRIEFING_AI": "1",
|
||||
# 일별 안정성 티어 (results_stable)
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_MAX_LOSING_DAYS": str(OPTUNA_STABLE_MAX_LOSING_DAYS_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_MIN_WORST_DAY_PNL": str(OPTUNA_STABLE_MIN_WORST_DAY_PNL_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_LAMBDA": str(OPTUNA_STABLE_LAMBDA_DEFAULT),
|
||||
"PARAM_SEARCH_OPTUNA_STABLE_MIN_ACTIVE_DAYS": str(OPTUNA_STABLE_MIN_ACTIVE_DAYS_DEFAULT),
|
||||
# Optuna apply 시 다단트레일 추천 → 전략별 *_DAILY_PROFIT_* (탐색 축 아님)
|
||||
"OPTUNA_DAILY_TRAIL_APPLY_ON_BEST": "true",
|
||||
"OPTUNA_DAILY_TRAIL_ARM_FRAC": "0.60",
|
||||
"OPTUNA_DAILY_TRAIL_BEST_FRAC": "0.70",
|
||||
"OPTUNA_DAILY_TRAIL_ARM_STEP": "5000",
|
||||
"OPTUNA_DAILY_TRAIL_MIN_ARM": "10000",
|
||||
"OPTUNA_DAILY_TRAIL_TIER_DROPS": "40,30,20",
|
||||
}
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
from database import TradeDB
|
||||
except ImportError:
|
||||
return
|
||||
owned = False
|
||||
if db is None:
|
||||
db = TradeDB()
|
||||
owned = True
|
||||
try:
|
||||
snap = db.get_merged_env_snapshot() or {}
|
||||
patch = {}
|
||||
for k, v in defaults.items():
|
||||
cur = snap.get(k)
|
||||
if cur is None or str(cur).strip() == "":
|
||||
patch[k] = v
|
||||
if not patch:
|
||||
return
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
n = db._persist_env_config_overflow(patch, now)
|
||||
try:
|
||||
from kis_trader.utils.env import invalidate_merged_env_cache
|
||||
invalidate_merged_env_cache()
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(
|
||||
"📌 Optuna 게이트 기본값 DB(ext) 반영 %d키: %s",
|
||||
n, sorted(patch.keys()),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("⚠️ Optuna 게이트 기본값 DB 반영 실패: %s", exc)
|
||||
finally:
|
||||
if owned:
|
||||
try:
|
||||
db.conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def mariadb_creds() -> dict:
|
||||
@@ -182,6 +633,7 @@ def announce_optuna_json_path(
|
||||
"""
|
||||
결과 JSON 절대경로를 터미널·로그에 눈에 띄게 고지.
|
||||
또한 logs/optuna_<strategy>_<mode>_latest.jsonpath 에 기록 (없으면 strategy만).
|
||||
note 에 '최종' 이 포함되면 이전장/앞장 브리핑(.briefing.md) 생성.
|
||||
"""
|
||||
abs_path = os.path.abspath(str(out_path or "").strip())
|
||||
lg = log or logger
|
||||
@@ -207,5 +659,14 @@ def announce_optuna_json_path(
|
||||
f.write(abs_path + "\n")
|
||||
except OSError as exc:
|
||||
lg.warning("⚠️ jsonpath 사이드카 기록 실패: %s", exc)
|
||||
|
||||
# 최종 JSON 저장 후 브리핑 (이전 장 / 앞으로 장)
|
||||
note_l = (note or "").strip()
|
||||
if "최종" in note_l and abs_path and os.path.isfile(abs_path):
|
||||
try:
|
||||
from kis_trader.backtest.optuna_briefing import write_briefing_for_json
|
||||
write_briefing_for_json(abs_path, log=lg)
|
||||
except Exception as exc:
|
||||
lg.warning("⚠️ Optuna 브리핑 실패: %s", exc)
|
||||
return abs_path
|
||||
|
||||
|
||||
Reference in New Issue
Block a user