이번에 들어간 내용

한투 호가 = 2번째 앱키 전용
키 없거나 start 실패 시 메인에 H0STASP0 안 붙임. 운영설정 WS_ORDERBOOK_SAVE_KIS 빨간 danger.

LS RAM 합집합
후보∪보유∪영구∪grace. sync_targets와 split reconcile 둘 다. 틱 DB 영구 게이트는 그대로.

분봉 쓰레기 → 다음 소스 봉 통째
그 분 틱 0건이거나 전부 봉끝 대비 LIVE_FEED_FALLBACK_MAX_AGE_SEC 초과면 구멍. 메인 WS → 2차 → LS → REST → rollup. CANDLE_GARBAGE_FALLBACK 기본 true.

파일: feed_fallback.py(신규), ws_manager.py, kis_ws.py, candle_series.py, bt_candle_source.py, live_config_schema.py, database.py, 스모크, MD 2개.

같은 ws_manager/database/kis_ws/live_config에는 직전 커밋 이후 쌓여 있던 시세 폴백·ENV 키 정리도 같이 들어갔습니다. 파일 단위로 나눌 수 없어서입니다.
This commit is contained in:
Your Name
2026-08-19 22:11:31 +09:00
parent d1dc274f0e
commit 0ecac7cb95
229 changed files with 8487 additions and 1647 deletions

View File

@@ -1,9 +1,8 @@
"""
kis_trader/backtest/optuna_orderbook_recommend.py
=================================================
Optuna 차트 캔들 최적화(Stage 1)가 완료된 후, 후처리(Stage 2)로 1,000회 고속 호가 탐색을 수행하여
전략(모멘텀/돌파 등)별 최적의 진입 호가필터 & 수익구간 호가매도 합의 수치(Consensus)를 도출하고
Optuna out_data 및 Apply 패치에 자동으로 결합하는 핵심 모듈입니다.
Optuna 차트 캔들 최적화(Stage 1)가 완료된 후, 후처리(Stage 2)로 진입/익절/손절 켜기·끄기 8방(방 안 TPE)을
돌린다. 시뮬은 켠 축만 스택(진입→익절호가→손절호가). 실매 엔진은 수정하지 않는다.
"""
from __future__ import annotations
@@ -319,11 +318,29 @@ def raw_fills_to_ob_trades(
def _ob_axis_n_trials(n_trials: int) -> int:
"""레거시: 축 독립 500회. 8방 경로(_ob_combo_n_trials)에서는 미사용."""
from kis_trader.utils.env import get_env_int
axis = int(get_env_int("OPTUNA_OB_AXIS_TRIALS", 0) or 0)
if axis > 0:
return max(10, axis)
return max(10, int(n_trials or _ob_n_trials(1000)))
return max(10, int(n_trials or _ob_n_trials(500)))
def _ob_combo_n_trials(n_on: int) -> int:
"""8방 중 켜진 축 개수별 trial. 축당 500×8 금지 — 합이 구 1,500 근방."""
from kis_trader.utils.env import get_env_int
n = int(n_on or 0)
if n <= 0:
return 0
if n == 1:
raw = int(get_env_int("OPTUNA_OB_COMBO_TRIALS_SINGLE", 150) or 0)
return max(10, raw if raw > 0 else 150)
if n == 2:
raw = int(get_env_int("OPTUNA_OB_COMBO_TRIALS_DOUBLE", 200) or 0)
return max(10, raw if raw > 0 else 200)
raw = int(get_env_int("OPTUNA_OB_COMBO_TRIALS_TRIPLE", 250) or 0)
return max(10, raw if raw > 0 else 250)
def _ob_orig_stats(trades: List[TradeInfo]) -> Dict[str, Any]:
@@ -440,6 +457,238 @@ def _sim_stop_ob(tr: TradeInfo, p: Dict[str, Any]) -> Tuple[float, float, str]:
return (tr.actual_pnl, tr.actual_profit_rate, "HOLD_TO_ORIG")
def _entry_sim_p(p: Dict[str, Any]) -> Dict[str, Any]:
return {
"max_spread_pct": p.get("max_spread_pct", p.get("orderbook_max_spread_pct")),
"min_bid_ask_ratio": p.get("min_bid_ask_ratio", p.get("orderbook_min_bid_ask_ratio")),
"ask_max_mult": p.get("ask_max_mult", p.get("orderbook_entry_ask_max_mult")),
}
def _flag_on(p: Dict[str, Any], raw_key: str, enabled_key: str) -> bool:
if raw_key in p:
return bool(p.get(raw_key))
return bool(p.get(enabled_key))
def _sim_stacked(tr: TradeInfo, p: Dict[str, Any]) -> Tuple[float, float, str]:
"""켜진 축만. 진입 탈락 → 보유 중 익절호가 → 손절호가 (실매 OB 순서)."""
use_e = _flag_on(p, "entry_on", "orderbook_filter_enabled")
use_x = _flag_on(p, "exit_on", "exit_ob_enabled")
use_s = _flag_on(p, "stop_on", "stop_ob_enabled")
if use_e:
ep = _entry_sim_p(p)
if ep.get("max_spread_pct") is None or ep.get("min_bid_ask_ratio") is None:
return (0.0, 0.0, "ENTRY_REJECTED")
r = _sim_entry(tr, ep)
if r[2] == "ENTRY_REJECTED":
return r
if not (use_x or use_s):
return (tr.actual_pnl, tr.actual_profit_rate, "ORIGINAL")
from kis_trader.engine.momentum_hts_logic import _check_exit_ob_l3, _check_stop_ob
exit_p = None
stop_p = None
if use_x:
exit_p = {
"exit_ob_enabled": True,
"exit_ob_ratio_min": float(p.get("exit_ob_ratio_min") or p.get("ob_ratio_min") or 0.4),
"exit_ob_ma_window": int(p.get("exit_ob_ma_window") or p.get("ma_window") or 5),
"exit_ob_min_profit_pct": float(p.get("exit_ob_min_profit_pct") or p.get("min_profit_pct") or 0.005),
"exit_ob_min_hold_bars": int(p.get("exit_ob_min_hold_bars") or p.get("min_hold_bars") or 3),
}
if use_s:
stop_p = {
"stop_ob_enabled": True,
"stop_ob_ratio_min": float(p.get("stop_ob_ratio_min") or 0.4),
"stop_ob_ma_window": int(p.get("stop_ob_ma_window") or 5),
"stop_ob_min_loss_pct": float(p.get("stop_ob_min_loss_pct") or 0.003),
"stop_ob_min_hold_bars": int(p.get("stop_ob_min_hold_bars") or 2),
}
history: List[Optional[float]] = []
for s in tr.holding_snaps:
cur_p, ratio = _snap_or_and_price(s)
if cur_p <= 0:
history.append(None)
continue
history.append(ratio)
hold_bars = max(0, int((s.t - tr.buy_dt).total_seconds() / 60.0))
if exit_p and _check_exit_ob_l3(exit_p, history, tr.buy_price, cur_p, hold_bars):
realized = (cur_p - tr.buy_price) * tr.qty
rate = ((cur_p - tr.buy_price) / tr.buy_price) * 100.0 if tr.buy_price else 0.0
return (realized, rate, "OB_EXIT")
if stop_p and _check_stop_ob(stop_p, history, tr.buy_price, cur_p, hold_bars):
realized = (cur_p - tr.buy_price) * tr.qty
rate = ((cur_p - tr.buy_price) / tr.buy_price) * 100.0 if tr.buy_price else 0.0
return (realized, rate, "OB_STOP")
return (tr.actual_pnl, tr.actual_profit_rate, "HOLD_TO_ORIG")
def _finalize_combo_params(
p: Dict[str, Any],
*,
use_e: bool,
use_x: bool,
use_s: bool,
) -> Dict[str, Any]:
out: Dict[str, Any] = {}
if use_e:
out["orderbook_filter_enabled"] = True
out["orderbook_max_spread_pct"] = p.get("max_spread_pct", p.get("orderbook_max_spread_pct"))
out["orderbook_min_bid_ask_ratio"] = p.get("min_bid_ask_ratio", p.get("orderbook_min_bid_ask_ratio"))
out["orderbook_entry_ask_max_mult"] = p.get("ask_max_mult", p.get("orderbook_entry_ask_max_mult"))
else:
out["orderbook_filter_enabled"] = False
if use_x:
out["exit_ob_enabled"] = True
out["exit_ob_min_hold_bars"] = p.get("exit_ob_min_hold_bars")
out["exit_ob_ratio_min"] = p.get("exit_ob_ratio_min")
out["exit_ob_min_profit_pct"] = p.get("exit_ob_min_profit_pct")
out["exit_ob_ma_window"] = p.get("exit_ob_ma_window")
else:
out["exit_ob_enabled"] = False
if use_s:
out["stop_ob_enabled"] = True
out["stop_ob_min_hold_bars"] = p.get("stop_ob_min_hold_bars")
out["stop_ob_ratio_min"] = p.get("stop_ob_ratio_min")
out["stop_ob_min_loss_pct"] = p.get("stop_ob_min_loss_pct")
out["stop_ob_ma_window"] = p.get("stop_ob_ma_window")
else:
out["stop_ob_enabled"] = False
return out
def _suggest_combo(
trial: Any,
*,
use_e: bool,
use_x: bool,
use_s: bool,
) -> Dict[str, Any]:
from kis_trader.utils.env import get_env_float, get_env_int
p: Dict[str, Any] = {"entry_on": bool(use_e), "exit_on": bool(use_x), "stop_on": bool(use_s)}
if use_e:
lo_s = float(get_env_float("OPTUNA_OB_ENTRY_SPREAD_MIN", 0.1))
hi_s = float(get_env_float("OPTUNA_OB_ENTRY_SPREAD_MAX", 8.0))
lo_r = float(get_env_float("OPTUNA_OB_ENTRY_RATIO_MIN", 0.05))
hi_r = float(get_env_float("OPTUNA_OB_ENTRY_RATIO_MAX", 1.5))
lo_a = float(get_env_float("OPTUNA_OB_ENTRY_ASK_MULT_MIN", 1.0))
hi_a = float(get_env_float("OPTUNA_OB_ENTRY_ASK_MULT_MAX", 80.0))
if hi_s < lo_s:
lo_s, hi_s = hi_s, lo_s
if hi_r < lo_r:
lo_r, hi_r = hi_r, lo_r
if hi_a < lo_a:
lo_a, hi_a = hi_a, lo_a
p["max_spread_pct"] = trial.suggest_float("max_spread_pct", lo_s, hi_s, step=0.1)
p["min_bid_ask_ratio"] = trial.suggest_float("min_bid_ask_ratio", lo_r, hi_r, step=0.05)
p["ask_max_mult"] = trial.suggest_float("ask_max_mult", lo_a, hi_a, step=1.0)
if use_x:
p["exit_ob_enabled"] = True
p["exit_ob_min_hold_bars"] = trial.suggest_int(
"exit_ob_min_hold_bars",
int(get_env_int("OPTUNA_OB_EXIT_HOLD_MIN", 1)),
int(get_env_int("OPTUNA_OB_EXIT_HOLD_MAX", 5)),
)
p["exit_ob_ratio_min"] = trial.suggest_float(
"exit_ob_ratio_min",
float(get_env_float("OPTUNA_OB_EXIT_RATIO_MIN", 0.2)),
float(get_env_float("OPTUNA_OB_EXIT_RATIO_MAX", 0.8)),
step=0.05,
)
p["exit_ob_min_profit_pct"] = trial.suggest_float(
"exit_ob_min_profit_pct",
float(get_env_float("OPTUNA_OB_EXIT_PROFIT_MIN", 0.003)),
float(get_env_float("OPTUNA_OB_EXIT_PROFIT_MAX", 0.02)),
step=0.001,
)
p["exit_ob_ma_window"] = trial.suggest_int(
"exit_ob_ma_window",
int(get_env_int("OPTUNA_OB_EXIT_MA_MIN", 3)),
int(get_env_int("OPTUNA_OB_EXIT_MA_MAX", 10)),
)
if use_s:
p["stop_ob_enabled"] = True
p["stop_ob_min_hold_bars"] = trial.suggest_int(
"stop_ob_min_hold_bars",
int(get_env_int("OPTUNA_OB_STOP_HOLD_MIN", 1)),
int(get_env_int("OPTUNA_OB_STOP_HOLD_MAX", 5)),
)
p["stop_ob_ratio_min"] = trial.suggest_float(
"stop_ob_ratio_min",
float(get_env_float("OPTUNA_OB_STOP_RATIO_MIN", 0.2)),
float(get_env_float("OPTUNA_OB_STOP_RATIO_MAX", 0.8)),
step=0.05,
)
p["stop_ob_min_loss_pct"] = trial.suggest_float(
"stop_ob_min_loss_pct",
float(get_env_float("OPTUNA_OB_STOP_LOSS_MIN", 0.001)),
float(get_env_float("OPTUNA_OB_STOP_LOSS_MAX", 0.02)),
step=0.001,
)
p["stop_ob_ma_window"] = trial.suggest_int(
"stop_ob_ma_window",
int(get_env_int("OPTUNA_OB_STOP_MA_MIN", 3)),
int(get_env_int("OPTUNA_OB_STOP_MA_MAX", 10)),
)
return p
def _optimize_combo(
trades: List[TradeInfo],
*,
orig_cnt: int,
n_trials: int,
lg: logging.Logger,
use_e: bool,
use_x: bool,
use_s: bool,
axis_name: str,
combo_id: str,
) -> Dict[str, Any]:
orig = _ob_orig_stats(trades)
mask = {"entry": bool(use_e), "exit": bool(use_x), "stop": bool(use_s)}
if int(n_trials or 0) <= 0:
return {
"ok": True,
"reason": "base_no_tpe",
"params": _finalize_combo_params({}, use_e=False, use_x=False, use_s=False),
"recommended_stats": orig,
"orig_stats": orig,
"n_trials": 0,
"combo_id": combo_id,
"mask": mask,
}
def suggest(trial: optuna.Trial) -> Dict[str, Any]:
return _suggest_combo(trial, use_e=use_e, use_x=use_x, use_s=use_s)
rec = _run_axis_study(
trades=trades,
orig_cnt=orig_cnt,
n_trials=n_trials,
suggest_fn=suggest,
sim_fn=_sim_stacked,
skip_reject=bool(use_e),
lg=lg,
axis_name=axis_name,
)
rec["combo_id"] = combo_id
rec["mask"] = mask
if rec.get("ok"):
rec["params"] = _finalize_combo_params(
rec.get("params") or {}, use_e=use_e, use_x=use_x, use_s=use_s,
)
rows = [_sim_stacked(t, rec["params"]) for t in trades]
rec_st = _suite_stats(rows, skip_reject=bool(use_e))
rec_st["pnl_diff"] = _krw_int(int(rec_st["pnl"]) - int(orig["pnl"]))
rec["recommended_stats"] = rec_st
rec["orig_stats"] = orig
return rec
def _run_axis_study(
*,
trades: List[TradeInfo],
@@ -464,7 +713,14 @@ def _run_axis_study(
return score
study = optuna.create_study(direction="maximize")
study.optimize(obj_func, n_trials=n_trials)
from kis_trader.backtest import optuna_post_progress as opp
def _cb(_study: Any, _trial: Any) -> None:
n = len(_study.trials)
if n == 1 or n == int(n_trials) or n % 5 == 0:
opp.on_ob_axis_trial(n, int(n_trials), axis_name)
study.optimize(obj_func, n_trials=n_trials, callbacks=[_cb])
valid_records.sort(key=lambda x: x["score"], reverse=True)
top5 = valid_records[: min(5, len(valid_records))]
if not top5:
@@ -495,114 +751,23 @@ def _run_axis_study(
def _optimize_entry_axis(trades: List[TradeInfo], *, orig_cnt: int, n_trials: int, lg: logging.Logger) -> Dict[str, Any]:
from kis_trader.utils.env import get_env_float
lo_s = float(get_env_float("OPTUNA_OB_ENTRY_SPREAD_MIN", 0.1))
hi_s = float(get_env_float("OPTUNA_OB_ENTRY_SPREAD_MAX", 8.0))
lo_r = float(get_env_float("OPTUNA_OB_ENTRY_RATIO_MIN", 0.05))
hi_r = float(get_env_float("OPTUNA_OB_ENTRY_RATIO_MAX", 1.5))
lo_a = float(get_env_float("OPTUNA_OB_ENTRY_ASK_MULT_MIN", 1.0))
hi_a = float(get_env_float("OPTUNA_OB_ENTRY_ASK_MULT_MAX", 80.0))
if hi_s < lo_s:
lo_s, hi_s = hi_s, lo_s
if hi_r < lo_r:
lo_r, hi_r = hi_r, lo_r
if hi_a < lo_a:
lo_a, hi_a = hi_a, lo_a
def suggest(trial: optuna.Trial) -> Dict[str, Any]:
return {
"max_spread_pct": trial.suggest_float("max_spread_pct", lo_s, hi_s, step=0.1),
"min_bid_ask_ratio": trial.suggest_float("min_bid_ask_ratio", lo_r, hi_r, step=0.05),
"ask_max_mult": trial.suggest_float("ask_max_mult", lo_a, hi_a, step=1.0),
}
rec = _run_axis_study(
trades=trades, orig_cnt=orig_cnt, n_trials=n_trials,
suggest_fn=suggest, sim_fn=_sim_entry, skip_reject=True, lg=lg, axis_name="진입호가",
return _optimize_combo(
trades, orig_cnt=orig_cnt, n_trials=n_trials, lg=lg,
use_e=True, use_x=False, use_s=False, axis_name="100 진입", combo_id="e",
)
if rec.get("ok"):
p = rec["params"]
rec["params"] = {
"orderbook_filter_enabled": True,
"orderbook_max_spread_pct": p.get("max_spread_pct"),
"orderbook_min_bid_ask_ratio": p.get("min_bid_ask_ratio"),
"orderbook_entry_ask_max_mult": p.get("ask_max_mult"),
}
return rec
def _optimize_exit_axis(trades: List[TradeInfo], *, orig_cnt: int, n_trials: int, lg: logging.Logger) -> Dict[str, Any]:
from kis_trader.utils.env import get_env_float, get_env_int
def suggest(trial: optuna.Trial) -> Dict[str, Any]:
on = trial.suggest_categorical("exit_ob_enabled", [True, False])
return {
"exit_ob_enabled": on,
"exit_ob_min_hold_bars": trial.suggest_int(
"exit_ob_min_hold_bars",
int(get_env_int("OPTUNA_OB_EXIT_HOLD_MIN", 1)),
int(get_env_int("OPTUNA_OB_EXIT_HOLD_MAX", 5)),
),
"exit_ob_ratio_min": trial.suggest_float(
"exit_ob_ratio_min",
float(get_env_float("OPTUNA_OB_EXIT_RATIO_MIN", 0.2)),
float(get_env_float("OPTUNA_OB_EXIT_RATIO_MAX", 0.8)),
step=0.05,
),
"exit_ob_min_profit_pct": trial.suggest_float(
"exit_ob_min_profit_pct",
float(get_env_float("OPTUNA_OB_EXIT_PROFIT_MIN", 0.003)),
float(get_env_float("OPTUNA_OB_EXIT_PROFIT_MAX", 0.02)),
step=0.001,
),
"exit_ob_ma_window": trial.suggest_int(
"exit_ob_ma_window",
int(get_env_int("OPTUNA_OB_EXIT_MA_MIN", 3)),
int(get_env_int("OPTUNA_OB_EXIT_MA_MAX", 10)),
),
}
return _run_axis_study(
trades=trades, orig_cnt=orig_cnt, n_trials=n_trials,
suggest_fn=suggest, sim_fn=_sim_exit_ob, skip_reject=False, lg=lg, axis_name="익절호가",
return _optimize_combo(
trades, orig_cnt=orig_cnt, n_trials=n_trials, lg=lg,
use_e=False, use_x=True, use_s=False, axis_name="010 익절", combo_id="x",
)
def _optimize_stop_axis(trades: List[TradeInfo], *, orig_cnt: int, n_trials: int, lg: logging.Logger) -> Dict[str, Any]:
from kis_trader.utils.env import get_env_float, get_env_int
def suggest(trial: optuna.Trial) -> Dict[str, Any]:
on = trial.suggest_categorical("stop_ob_enabled", [True, False])
return {
"stop_ob_enabled": on,
"stop_ob_min_hold_bars": trial.suggest_int(
"stop_ob_min_hold_bars",
int(get_env_int("OPTUNA_OB_STOP_HOLD_MIN", 1)),
int(get_env_int("OPTUNA_OB_STOP_HOLD_MAX", 5)),
),
"stop_ob_ratio_min": trial.suggest_float(
"stop_ob_ratio_min",
float(get_env_float("OPTUNA_OB_STOP_RATIO_MIN", 0.2)),
float(get_env_float("OPTUNA_OB_STOP_RATIO_MAX", 0.8)),
step=0.05,
),
"stop_ob_min_loss_pct": trial.suggest_float(
"stop_ob_min_loss_pct",
float(get_env_float("OPTUNA_OB_STOP_LOSS_MIN", 0.001)),
float(get_env_float("OPTUNA_OB_STOP_LOSS_MAX", 0.02)),
step=0.001,
),
"stop_ob_ma_window": trial.suggest_int(
"stop_ob_ma_window",
int(get_env_int("OPTUNA_OB_STOP_MA_MIN", 3)),
int(get_env_int("OPTUNA_OB_STOP_MA_MAX", 10)),
),
}
return _run_axis_study(
trades=trades, orig_cnt=orig_cnt, n_trials=n_trials,
suggest_fn=suggest, sim_fn=_sim_stop_ob, skip_reject=False, lg=lg, axis_name="손절호가",
return _optimize_combo(
trades, orig_cnt=orig_cnt, n_trials=n_trials, lg=lg,
use_e=False, use_x=False, use_s=True, axis_name="001 손절", combo_id="s",
)
@@ -620,7 +785,7 @@ def recommend_orderbook_parameters(
"""호가 후처리. raw_fills 있으면 그 체결만(백테 앵커). 없으면 trade_history(실매 참고행)."""
lg = log or logger
if int(n_trials or 0) <= 0:
n_trials = _ob_n_trials(1000)
n_trials = _ob_n_trials(500)
strat_upper = strategy.upper()
table, source_filter = resolve_orderbook_recommend_table(
ob_table=ob_table,
@@ -726,30 +891,72 @@ def recommend_orderbook_parameters(
orig_stats = _ob_orig_stats(trades)
orig_cnt = int(orig_stats["count"])
axis_n = _ob_axis_n_trials(int(n_trials))
can_exit_stop = strat_upper in ("MOMENTUM", "BREAKOUT")
entry_axis = _optimize_entry_axis(trades, orig_cnt=orig_cnt, n_trials=axis_n, lg=lg)
exit_axis: Dict[str, Any]
stop_axis: Dict[str, Any]
combo_specs: List[Tuple[str, str, bool, bool, bool]] = [
("e", "100 진입", True, False, False),
]
if can_exit_stop:
exit_axis = _optimize_exit_axis(trades, orig_cnt=orig_cnt, n_trials=axis_n, lg=lg)
stop_axis = _optimize_stop_axis(trades, orig_cnt=orig_cnt, n_trials=axis_n, lg=lg)
else:
exit_axis = {"ok": False, "reason": "n/a_strategy", "params": {}, "recommended_stats": {}}
stop_axis = {"ok": False, "reason": "n/a_strategy", "params": {}, "recommended_stats": {}}
combo_specs.extend(
[
("x", "010 익절", False, True, False),
("s", "001 손절", False, False, True),
("ex", "110 진입+익절", True, True, False),
("es", "101 진입+손절", True, False, True),
("xs", "011 익절+손절", False, True, True),
("exs", "111 전부", True, True, True),
]
)
from kis_trader.backtest import optuna_post_progress as opp
n_ax = len(combo_specs)
opp.set_ob_axes(n_ax, 0)
combos: Dict[str, Any] = {
"base": {
"ok": True,
"reason": "base_no_tpe",
"params": {},
"recommended_stats": orig_stats,
"orig_stats": orig_stats,
"n_trials": 0,
"combo_id": "base",
"mask": {"entry": False, "exit": False, "stop": False},
}
}
for i, (cid, label, use_e, use_x, use_s) in enumerate(combo_specs):
n_on = int(use_e) + int(use_x) + int(use_s)
n_tr = _ob_combo_n_trials(n_on)
opp.begin_ob_axis(label, i, n_tr)
combos[cid] = _optimize_combo(
trades,
orig_cnt=orig_cnt,
n_trials=n_tr,
lg=lg,
use_e=use_e,
use_x=use_x,
use_s=use_s,
axis_name=label,
combo_id=cid,
)
def _pick(*ids: str) -> Dict[str, Any]:
for i in ids:
c = combos.get(i)
if isinstance(c, dict) and c.get("ok"):
return c
return {"ok": False, "reason": "no_combo", "params": {}, "recommended_stats": orig_stats}
# 적용 버튼: 진입=100 · 익절까지=110 스택 · 손절까지=111 스택
entry_axis = _pick("e")
exit_axis = _pick("ex", "x") if can_exit_stop else {"ok": False, "reason": "n/a_strategy", "params": {}, "recommended_stats": {}}
stop_axis = _pick("exs", "es", "xs", "s") if can_exit_stop else {"ok": False, "reason": "n/a_strategy", "params": {}, "recommended_stats": {}}
merged_params: Dict[str, Any] = {}
if entry_axis.get("ok"):
merged_params.update(entry_axis.get("params") or {})
if exit_axis.get("ok"):
merged_params.update(exit_axis.get("params") or {})
if stop_axis.get("ok"):
merged_params.update(stop_axis.get("params") or {})
for ax in (entry_axis, exit_axis, stop_axis):
if ax.get("ok"):
merged_params.update(ax.get("params") or {})
rec_stats = dict(entry_axis.get("recommended_stats") or orig_stats)
rec_stats = dict(stop_axis.get("recommended_stats") or exit_axis.get("recommended_stats") or entry_axis.get("recommended_stats") or orig_stats)
lg.info(
"⚡ [호가 축분리] 전략=%s 모수=%d entry=%s exit=%s stop=%s",
"⚡ [호가 8방] 전략=%s 모수=%d e=%s ex=%s exs=%s",
strat_upper,
orig_cnt,
"ok" if entry_axis.get("ok") else entry_axis.get("reason"),
@@ -757,16 +964,17 @@ def recommend_orderbook_parameters(
"ok" if stop_axis.get("ok") else stop_axis.get("reason"),
)
return {
"ok": bool(entry_axis.get("ok") or exit_axis.get("ok") or stop_axis.get("ok")),
"ok": any(bool((combos.get(cid) or {}).get("ok")) for cid, *_rest in combo_specs),
"strategy": strat_upper,
"ob_table": table,
"n_trials": axis_n,
"n_trials": sum(int((combos.get(c[0]) or {}).get("n_trials") or 0) for c in combo_specs),
"trade_count": orig_cnt,
"orig_stats": orig_stats,
"recommended_stats": rec_stats,
"entry": entry_axis,
"exit": exit_axis,
"stop": stop_axis,
"combos": combos,
"params": merged_params,
}