이번에 들어간 내용
한투 호가 = 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:
@@ -40,6 +40,10 @@ def _include_live() -> bool:
|
||||
return bool(get_env_bool("OPTUNA_POST_INCLUDE_LIVE", True))
|
||||
|
||||
|
||||
def _include_stable() -> bool:
|
||||
return bool(get_env_bool("OPTUNA_POST_INCLUDE_STABLE", True))
|
||||
|
||||
|
||||
def _run_ob_whipsaw_full() -> bool:
|
||||
"""Optuna 최종 저장 기본 ON. 웹 light 경로는 호출측에서 False."""
|
||||
return bool(get_env_bool("OPTUNA_POST_RUN_OB_WHIPSAW", True))
|
||||
@@ -77,12 +81,16 @@ def _slim_axis(ax: Any) -> Dict[str, Any]:
|
||||
"recommended_stats": rs,
|
||||
"orig_stats": os_,
|
||||
"n_trials": ax.get("n_trials"),
|
||||
"combo_id": ax.get("combo_id") or "",
|
||||
"mask": dict(ax.get("mask") or {}),
|
||||
}
|
||||
|
||||
|
||||
def _slim_ob(rec: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
if not isinstance(rec, dict):
|
||||
return {"ok": False, "reason": "none"}
|
||||
combos_in = rec.get("combos") if isinstance(rec.get("combos"), dict) else {}
|
||||
combos_out = {str(k): _slim_axis(v) for k, v in combos_in.items()}
|
||||
out = {
|
||||
"ok": bool(rec.get("ok")),
|
||||
"reason": rec.get("reason") or "",
|
||||
@@ -93,6 +101,7 @@ def _slim_ob(rec: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"entry": _slim_axis(rec.get("entry")),
|
||||
"exit": _slim_axis(rec.get("exit")),
|
||||
"stop": _slim_axis(rec.get("stop")),
|
||||
"combos": combos_out,
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -230,6 +239,7 @@ def _consensus_axis(pool: List[Dict[str, Any]], axis: str, keys: Tuple[str, ...]
|
||||
params_list: List[Dict[str, Any]] = []
|
||||
stats_pnls: List[int] = []
|
||||
stats_cnt: List[int] = []
|
||||
stats_wrs: List[float] = []
|
||||
for a in pool:
|
||||
ob = a.get("orderbook") or {}
|
||||
nested = ob.get(axis) if isinstance(ob.get(axis), dict) else {}
|
||||
@@ -238,6 +248,10 @@ def _consensus_axis(pool: List[Dict[str, Any]], axis: str, keys: Tuple[str, ...]
|
||||
rs = nested.get("recommended_stats") or {}
|
||||
stats_pnls.append(_krw_int(rs.get("pnl")))
|
||||
stats_cnt.append(int(rs.get("count") or 0))
|
||||
try:
|
||||
stats_wrs.append(float(rs.get("win_rate")))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
elif axis == "entry" and ob.get("ok") and not nested:
|
||||
# 구 JSON: 합쳐진 params
|
||||
p = dict(ob.get("params") or {})
|
||||
@@ -263,30 +277,205 @@ def _consensus_axis(pool: List[Dict[str, Any]], axis: str, keys: Tuple[str, ...]
|
||||
"count": int(statistics.median(stats_cnt)) if stats_cnt else 0,
|
||||
"pnl": _krw_int(statistics.median(stats_pnls)),
|
||||
}
|
||||
if stats_wrs:
|
||||
rec_st["win_rate"] = round(float(statistics.median(stats_wrs)), 1)
|
||||
return {"ok": bool(cons), "params": cons, "n": len(params_list), "recommended_stats": rec_st}
|
||||
|
||||
|
||||
_COMBO_LABELS: Dict[str, str] = {
|
||||
"base": "000 타점만",
|
||||
"e": "100 진입만",
|
||||
"x": "010 익절만",
|
||||
"s": "001 손절만",
|
||||
"ex": "110 진입+익절",
|
||||
"es": "101 진입+손절",
|
||||
"xs": "011 익절+손절",
|
||||
"exs": "111 전부",
|
||||
}
|
||||
# 호가 8방 = 진입×익절×손절 (2³). 휩쏘는 8방 밖.
|
||||
_COMBO_IDS = ("base", "e", "x", "s", "ex", "es", "xs", "exs")
|
||||
_COMBO_MASK = {
|
||||
"base": (False, False, False),
|
||||
"e": (True, False, False),
|
||||
"x": (False, True, False),
|
||||
"s": (False, False, True),
|
||||
"ex": (True, True, False),
|
||||
"es": (True, False, True),
|
||||
"xs": (False, True, True),
|
||||
"exs": (True, True, True),
|
||||
}
|
||||
|
||||
|
||||
def _median_params(params_list: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
if not params_list:
|
||||
return {}
|
||||
all_keys: set = set()
|
||||
for p in params_list:
|
||||
all_keys.update(p.keys())
|
||||
bool_keys = {
|
||||
"orderbook_filter_enabled", "exit_ob_enabled", "stop_ob_enabled",
|
||||
"whipsaw_filter_enabled", "entry_on", "exit_on", "stop_on",
|
||||
}
|
||||
int_keys = {
|
||||
"exit_ob_min_hold_bars", "exit_ob_ma_window",
|
||||
"stop_ob_min_hold_bars", "stop_ob_ma_window",
|
||||
"whipsaw_subbar_sec", "whipsaw_lookback_sec",
|
||||
}
|
||||
cons: Dict[str, Any] = {}
|
||||
for k in all_keys:
|
||||
vs = [p[k] for p in params_list if k in p and p[k] is not None]
|
||||
if not vs:
|
||||
continue
|
||||
if k in bool_keys or all(isinstance(v, bool) for v in vs):
|
||||
cons[k] = sum(1 for v in vs if v) >= (len(vs) / 2.0)
|
||||
elif k in int_keys or all(isinstance(v, int) and not isinstance(v, bool) for v in vs):
|
||||
m = _median_num(vs)
|
||||
cons[k] = int(m) if m is not None else vs[0]
|
||||
else:
|
||||
m = _median_num(vs)
|
||||
cons[k] = round(m, 4) if m is not None else vs[0]
|
||||
return cons
|
||||
|
||||
|
||||
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):
|
||||
return {}
|
||||
raw = ob.get("combos")
|
||||
if isinstance(raw, dict) and raw:
|
||||
return {str(k): dict(v) for k, v in raw.items() if isinstance(v, dict)}
|
||||
out: Dict[str, Dict[str, Any]] = {}
|
||||
for cid, nest_key in (("e", "entry"), ("x", "exit"), ("s", "stop")):
|
||||
nested = ob.get(nest_key)
|
||||
if isinstance(nested, dict) and nested.get("ok"):
|
||||
out[cid] = nested
|
||||
if not out and ob.get("ok") and ob.get("params"):
|
||||
out["e"] = {"ok": True, "params": dict(ob.get("params") or {}), "recommended_stats": ob.get("recommended_stats") or {}}
|
||||
return out
|
||||
|
||||
|
||||
def _combo_median_stats(recs: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
stats_pnls: List[int] = []
|
||||
stats_cnt: List[int] = []
|
||||
stats_wrs: List[float] = []
|
||||
for r in recs:
|
||||
rs = r.get("recommended_stats") or {}
|
||||
stats_pnls.append(_krw_int(rs.get("pnl")))
|
||||
stats_cnt.append(int(rs.get("count") or 0))
|
||||
try:
|
||||
stats_wrs.append(float(rs.get("win_rate")))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if not stats_pnls:
|
||||
return {}
|
||||
out: Dict[str, Any] = {
|
||||
"count": int(statistics.median(stats_cnt)) if stats_cnt else 0,
|
||||
"pnl": _krw_int(statistics.median(stats_pnls)),
|
||||
}
|
||||
if stats_wrs:
|
||||
out["win_rate"] = round(float(statistics.median(stats_wrs)), 1)
|
||||
return out
|
||||
|
||||
|
||||
def _axis_slice_from_combo(
|
||||
combo_params: Dict[str, Any],
|
||||
*,
|
||||
use: bool,
|
||||
axis: str,
|
||||
n: int,
|
||||
rec_st: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
if not use:
|
||||
return {"ok": False, "params": {}, "recommended_stats": {}, "n": 0}
|
||||
p = dict(combo_params or {})
|
||||
if axis == "entry":
|
||||
keys = ("orderbook_filter_enabled", "orderbook_max_spread_pct", "orderbook_min_bid_ask_ratio", "orderbook_entry_ask_max_mult")
|
||||
elif axis == "exit":
|
||||
keys = ("exit_ob_enabled", "exit_ob_min_hold_bars", "exit_ob_min_profit_pct", "exit_ob_ratio_min", "exit_ob_ma_window")
|
||||
else:
|
||||
keys = ("stop_ob_enabled", "stop_ob_min_hold_bars", "stop_ob_min_loss_pct", "stop_ob_ratio_min", "stop_ob_ma_window")
|
||||
sub = {k: p[k] for k in keys if k in p}
|
||||
ok = bool(sub) and (axis != "entry" or sub.get("orderbook_max_spread_pct") is not None)
|
||||
return {"ok": ok, "params": sub, "recommended_stats": dict(rec_st), "n": n}
|
||||
|
||||
|
||||
def _consensus_from_anchors(anchors: List[Dict[str, Any]], strategy: str) -> Dict[str, Any]:
|
||||
"""gated+mode 만. live 제외. 축=entry/exit/stop/whipsaw/trail."""
|
||||
pool = [a for a in anchors if str(a.get("role") or "") != "live"]
|
||||
entry = _consensus_axis(
|
||||
pool, "entry",
|
||||
("orderbook_filter_enabled", "orderbook_max_spread_pct", "orderbook_min_bid_ask_ratio", "orderbook_entry_ask_max_mult"),
|
||||
("orderbook_filter_enabled",),
|
||||
(),
|
||||
)
|
||||
exit_c = _consensus_axis(
|
||||
pool, "exit",
|
||||
("exit_ob_enabled", "exit_ob_min_hold_bars", "exit_ob_min_profit_pct", "exit_ob_ratio_min", "exit_ob_ma_window"),
|
||||
("exit_ob_enabled",),
|
||||
("exit_ob_min_hold_bars", "exit_ob_ma_window"),
|
||||
)
|
||||
stop_c = _consensus_axis(
|
||||
pool, "stop",
|
||||
("stop_ob_enabled", "stop_ob_min_hold_bars", "stop_ob_min_loss_pct", "stop_ob_ratio_min", "stop_ob_ma_window"),
|
||||
("stop_ob_enabled",),
|
||||
("stop_ob_min_hold_bars", "stop_ob_ma_window"),
|
||||
)
|
||||
"""gated+mode. live·stable 제외. 호가=8방 중 median PnL 최고 방 + median 파라미터."""
|
||||
pool = [a for a in anchors if str(a.get("role") or "") in ("gated", "mode")]
|
||||
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 {})
|
||||
for cid in _COMBO_IDS:
|
||||
c = combos.get(cid)
|
||||
if isinstance(c, dict) and c.get("ok"):
|
||||
by_combo[cid].append(c)
|
||||
|
||||
best_cid: Optional[str] = None
|
||||
best_med_pnl = -10**15
|
||||
for cid in _COMBO_IDS:
|
||||
if cid == "base":
|
||||
continue
|
||||
recs = by_combo.get(cid) or []
|
||||
if not recs:
|
||||
continue
|
||||
pnls = [_krw_int((r.get("recommended_stats") or {}).get("pnl")) for r in recs]
|
||||
med = float(statistics.median(pnls)) if pnls else -10**15
|
||||
if med > best_med_pnl:
|
||||
best_med_pnl = med
|
||||
best_cid = cid
|
||||
|
||||
combo_cons: Dict[str, Any] = {"ok": False, "combo_id": "", "label": "", "params": {}, "n": 0}
|
||||
entry: Dict[str, Any] = {"ok": False, "params": {}, "n": 0}
|
||||
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 파라미터. 실매 참고행 제외."
|
||||
|
||||
if best_cid:
|
||||
recs = by_combo[best_cid]
|
||||
params = _median_params([dict(r.get("params") or {}) for r in recs])
|
||||
use_e, use_x, use_s = _COMBO_MASK[best_cid]
|
||||
params["orderbook_filter_enabled"] = bool(use_e)
|
||||
params["exit_ob_enabled"] = bool(use_x)
|
||||
params["stop_ob_enabled"] = bool(use_s)
|
||||
rec_st = _combo_median_stats(recs)
|
||||
combo_cons = {
|
||||
"ok": True,
|
||||
"combo_id": best_cid,
|
||||
"label": _COMBO_LABELS.get(best_cid, best_cid),
|
||||
"mask": {"entry": bool(use_e), "exit": bool(use_x), "stop": bool(use_s)},
|
||||
"params": params,
|
||||
"recommended_stats": rec_st,
|
||||
"n": len(recs),
|
||||
}
|
||||
ob_merged = dict(params)
|
||||
entry = _axis_slice_from_combo(params, use=use_e, axis="entry", n=len(recs), rec_st=rec_st)
|
||||
exit_c = _axis_slice_from_combo(params, use=use_x, axis="exit", n=len(recs), rec_st=rec_st)
|
||||
stop_c = _axis_slice_from_combo(params, use=use_s, axis="stop", n=len(recs), rec_st=rec_st)
|
||||
elif pool:
|
||||
# 구 JSON(8방 없음): 축별 median 폴백 — apply 구스크립트 호환
|
||||
entry = _consensus_axis(
|
||||
pool, "entry",
|
||||
("orderbook_filter_enabled", "orderbook_max_spread_pct", "orderbook_min_bid_ask_ratio", "orderbook_entry_ask_max_mult"),
|
||||
("orderbook_filter_enabled",),
|
||||
(),
|
||||
)
|
||||
exit_c = _consensus_axis(
|
||||
pool, "exit",
|
||||
("exit_ob_enabled", "exit_ob_min_hold_bars", "exit_ob_min_profit_pct", "exit_ob_ratio_min", "exit_ob_ma_window"),
|
||||
("exit_ob_enabled",),
|
||||
("exit_ob_min_hold_bars", "exit_ob_ma_window"),
|
||||
)
|
||||
stop_c = _consensus_axis(
|
||||
pool, "stop",
|
||||
("stop_ob_enabled", "stop_ob_min_hold_bars", "stop_ob_min_loss_pct", "stop_ob_ratio_min", "stop_ob_ma_window"),
|
||||
("stop_ob_enabled",),
|
||||
("stop_ob_min_hold_bars", "stop_ob_ma_window"),
|
||||
)
|
||||
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방 없음). 후처리 재실행 권장."
|
||||
|
||||
ws_params_list = [
|
||||
dict((a.get("whipsaw") or {}).get("params") or {})
|
||||
@@ -296,18 +485,9 @@ def _consensus_from_anchors(anchors: List[Dict[str, Any]], strategy: str) -> Dic
|
||||
|
||||
ws_cons: Dict[str, Any] = {}
|
||||
if ws_params_list:
|
||||
for k in ("whipsaw_subbar_sec", "whipsaw_lookback_sec", "whipsaw_dip_pct", "whipsaw_filter_enabled"):
|
||||
vs = [p.get(k) for p in ws_params_list if k in p]
|
||||
if not vs:
|
||||
continue
|
||||
if k == "whipsaw_filter_enabled":
|
||||
ws_cons[k] = True
|
||||
elif k == "whipsaw_dip_pct":
|
||||
m = _median_num(vs)
|
||||
ws_cons[k] = round(m, 4) if m is not None else vs[0]
|
||||
else:
|
||||
m = _median_num(vs)
|
||||
ws_cons[k] = int(m) if m is not None else vs[0]
|
||||
ws_cons = _median_params(ws_params_list)
|
||||
if ws_cons:
|
||||
ws_cons["whipsaw_filter_enabled"] = True
|
||||
|
||||
trail_cons: Dict[str, Any] = {"ok": False}
|
||||
if trail_ok:
|
||||
@@ -327,19 +507,21 @@ def _consensus_from_anchors(anchors: List[Dict[str, Any]], strategy: str) -> Dic
|
||||
"note": "gated+mode 합의(median/최빈). 실매 행 제외. 타점 적용과 별도 버튼.",
|
||||
}
|
||||
|
||||
ob_merged = dict(entry.get("params") or {})
|
||||
ob_merged.update(exit_c.get("params") or {})
|
||||
ob_merged.update(stop_c.get("params") or {})
|
||||
|
||||
return {
|
||||
"combo": combo_cons,
|
||||
"entry": entry,
|
||||
"exit": exit_c,
|
||||
"stop": stop_c,
|
||||
"orderbook": {"ok": bool(ob_merged), "params": ob_merged, "n": entry.get("n") or 0},
|
||||
"orderbook": {
|
||||
"ok": bool(ob_merged),
|
||||
"params": ob_merged,
|
||||
"combo_id": combo_cons.get("combo_id") or "",
|
||||
"n": combo_cons.get("n") or entry.get("n") or 0,
|
||||
},
|
||||
"whipsaw": {"ok": bool(ws_cons), "params": ws_cons, "n": len(ws_params_list)},
|
||||
"trail": trail_cons,
|
||||
"strategy": strategy,
|
||||
"note": "합의=Top5(+mode) median/최빈. 실매 참고행은 제외.",
|
||||
"note": note,
|
||||
}
|
||||
|
||||
|
||||
@@ -360,7 +542,7 @@ def _dispersion_points(vals: List[float]) -> Tuple[float, str]:
|
||||
|
||||
|
||||
def _postprocess_overfit_extra(anchors: List[Dict[str, Any]]) -> Tuple[float, List[Dict[str, Any]]]:
|
||||
pool = [a for a in anchors if str(a.get("role") or "") != "live"]
|
||||
pool = [a for a in anchors if str(a.get("role") or "") in ("gated", "mode")]
|
||||
factors: List[Dict[str, Any]] = []
|
||||
extra = 0.0
|
||||
|
||||
@@ -371,11 +553,31 @@ def _postprocess_overfit_extra(anchors: List[Dict[str, Any]]) -> Tuple[float, Li
|
||||
dips = []
|
||||
arms = []
|
||||
for a in pool:
|
||||
op = (a.get("orderbook") or {}).get("params") or {}
|
||||
entry_p = ((a.get("orderbook") or {}).get("entry") or {}).get("params") or op
|
||||
exit_p = ((a.get("orderbook") or {}).get("exit") or {}).get("params") or {}
|
||||
stop_p = ((a.get("orderbook") or {}).get("stop") or {}).get("params") or {}
|
||||
if (a.get("orderbook") or {}).get("ok") or ((a.get("orderbook") or {}).get("entry") or {}).get("ok"):
|
||||
ob = a.get("orderbook") or {}
|
||||
combos = _anchor_combos_map(ob)
|
||||
entry_p: Dict[str, Any] = {}
|
||||
exit_p: Dict[str, Any] = {}
|
||||
stop_p: Dict[str, Any] = {}
|
||||
if combos:
|
||||
for cid, keys, dest in (
|
||||
("e", ("orderbook_max_spread_pct", "orderbook_min_bid_ask_ratio"), entry_p),
|
||||
("x", ("exit_ob_ratio_min",), exit_p),
|
||||
("s", ("stop_ob_ratio_min",), stop_p),
|
||||
):
|
||||
c = combos.get(cid) or {}
|
||||
if c.get("ok"):
|
||||
p = dict(c.get("params") or {})
|
||||
for k in keys:
|
||||
if p.get(k) is not None:
|
||||
dest[k] = p[k]
|
||||
if not entry_p:
|
||||
op = ob.get("params") or {}
|
||||
entry_p = dict((ob.get("entry") or {}).get("params") or op)
|
||||
if not exit_p:
|
||||
exit_p = dict((ob.get("exit") or {}).get("params") or {})
|
||||
if not stop_p:
|
||||
stop_p = dict((ob.get("stop") or {}).get("params") or {})
|
||||
if ob.get("ok") or combos or ((ob.get("entry") or {}).get("ok")):
|
||||
if entry_p.get("orderbook_max_spread_pct") is not None:
|
||||
spreads.append(float(entry_p["orderbook_max_spread_pct"]))
|
||||
if entry_p.get("orderbook_min_bid_ask_ratio") is not None:
|
||||
@@ -413,6 +615,122 @@ def _verdict(risk: float) -> Tuple[str, str]:
|
||||
return "상대적으로낮음", "상대적으로 낮음"
|
||||
|
||||
|
||||
def _reuse_ob_ws(anchors: List[Dict[str, Any]], trial: Any) -> Optional[Dict[str, Any]]:
|
||||
if trial is None:
|
||||
return None
|
||||
try:
|
||||
tn = int(trial)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
for a in anchors:
|
||||
try:
|
||||
at = int(a.get("optuna_trial_number"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if at != tn:
|
||||
continue
|
||||
ob = a.get("orderbook") or {}
|
||||
if ob.get("ok") or (ob.get("entry") or {}).get("ok"):
|
||||
return a
|
||||
return None
|
||||
|
||||
|
||||
def append_stable_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:
|
||||
"""results_stable TopN 을 후처리 표 앵커로 붙인다. 같은 trial 은 gated 호가 재사용."""
|
||||
if not _include_stable():
|
||||
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))]
|
||||
for i, row in enumerate(stable, start=1):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
combo = _combo_from_row(row)
|
||||
trial = row.get("optuna_trial_number") or row.get("_trial_number")
|
||||
reused = _reuse_ob_ws(anchors, trial)
|
||||
fills: List[Dict[str, Any]] = []
|
||||
pnl = row.get("total_pnl")
|
||||
trail = _slim_trail(_trail_for_pnl(strat, pnl))
|
||||
if do_ob:
|
||||
from kis_trader.backtest import optuna_post_progress as opp
|
||||
opp.next_unit(
|
||||
f"stable#{i}",
|
||||
"호가재사용" if reused is not None else "백테재실행·호가",
|
||||
)
|
||||
if reused is not None:
|
||||
ob = reused.get("orderbook") or {"ok": False, "reason": "light_skip"}
|
||||
ws = reused.get("whipsaw") or {"ok": False, "reason": "light_skip"}
|
||||
if do_ob:
|
||||
lg.info("📌 [후처리] stable#%d 호가 재사용 (gated와 동일 trial)", i)
|
||||
else:
|
||||
ob = {"ok": False, "reason": "light_skip"}
|
||||
ws = {"ok": False, "reason": "light_skip"}
|
||||
if do_ob and evaluate_fn is not None:
|
||||
lg.info("📌 [후처리] stable#%d 백테 재실행 (호가/휩쏘 체결)", i)
|
||||
fills = _replay_fills(evaluate_fn, combo, lg)
|
||||
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"stable#{i}",
|
||||
"role": "stable",
|
||||
"rank": i,
|
||||
"optuna_trial_number": trial,
|
||||
"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": "안정 후보" + (" · gated와 동일 trial 호가 재사용" if reused is not None else ""),
|
||||
})
|
||||
|
||||
|
||||
def ensure_stable_postprocess_on_payload(data: Dict[str, Any]) -> None:
|
||||
"""구 JSON(gated만 있는 후처리)에도 안정 앵커를 붙여 웹 표가 나오게."""
|
||||
topn = (data or {}).get("postprocess_topn")
|
||||
if not isinstance(topn, dict):
|
||||
return
|
||||
anchors = list(topn.get("postprocess_by_anchor") or [])
|
||||
if not anchors:
|
||||
return
|
||||
strat = str(data.get("strategy") or "momentum").strip().lower()
|
||||
strat_u = strat.upper()
|
||||
if strat_u in ("TAIL", "SHORT"):
|
||||
strat_u = "TAIL"
|
||||
elif strat_u in ("SCALPING", "SCALP"):
|
||||
strat_u = "SCALP"
|
||||
before = len(anchors)
|
||||
append_stable_postprocess_anchors(
|
||||
data, anchors,
|
||||
strat=strat, strat_u=strat_u, top_n=resolve_post_top_n(5),
|
||||
do_ob=False, evaluate_fn=None, lg=logger, ob_n_trials=0,
|
||||
)
|
||||
if len(anchors) != before:
|
||||
topn["postprocess_by_anchor"] = anchors
|
||||
|
||||
|
||||
def attach_topn_postprocess(
|
||||
out_data: Dict[str, Any],
|
||||
*,
|
||||
@@ -442,9 +760,22 @@ def attach_topn_postprocess(
|
||||
gated = list(data.get("results_gated") or [])[:top_n]
|
||||
anchors: List[Dict[str, Any]] = []
|
||||
|
||||
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)
|
||||
if _include_mode():
|
||||
n_units += 1
|
||||
if _include_live():
|
||||
n_units += 1
|
||||
if do_ob:
|
||||
opp.begin_job(lg, max(1, n_units))
|
||||
|
||||
for i, row in enumerate(gated, start=1):
|
||||
combo = _combo_from_row(row)
|
||||
fills: List[Dict[str, Any]] = []
|
||||
if do_ob:
|
||||
opp.next_unit(f"gated#{i}", "백테재실행·호가")
|
||||
if do_ob and evaluate_fn is not None:
|
||||
lg.info("📌 [후처리] gated#%d 백테 재실행 (호가/휩쏘 체결)", i)
|
||||
fills = _replay_fills(evaluate_fn, combo, lg)
|
||||
@@ -469,17 +800,28 @@ def attach_topn_postprocess(
|
||||
"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": "사후합격 후보",
|
||||
})
|
||||
|
||||
append_stable_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,
|
||||
)
|
||||
|
||||
if _include_mode():
|
||||
mc = data.get("mode_combo") or {}
|
||||
bt = mc.get("backtest") or {}
|
||||
mode_pnl = bt.get("total_pnl")
|
||||
fills_m = list(mode_fills or [])
|
||||
if do_ob:
|
||||
from kis_trader.backtest import optuna_post_progress as opp
|
||||
opp.next_unit("mode", "mode_combo 호가")
|
||||
if do_ob and not fills_m and evaluate_fn is not None:
|
||||
mode_params = dict(mc.get("params") or {})
|
||||
if mode_params:
|
||||
@@ -502,6 +844,8 @@ def attach_topn_postprocess(
|
||||
"optuna_trial_number": None,
|
||||
"total_pnl": _krw_int(mode_pnl),
|
||||
"total_trades": int(bt.get("total_trades") or 0),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
"orderbook": ob,
|
||||
"whipsaw": ws,
|
||||
"trail": trail,
|
||||
@@ -512,6 +856,8 @@ def attach_topn_postprocess(
|
||||
live_ob = {"ok": False, "reason": "light_skip"}
|
||||
live_ws = {"ok": False, "reason": "light_skip"}
|
||||
if do_ob:
|
||||
from kis_trader.backtest import optuna_post_progress as opp
|
||||
opp.next_unit("live", "실매참고 호가")
|
||||
live_ob = _ob_for_anchor(strategy=strat_u, out_data=data, fills=None, live=True, lg=lg, n_trials=ob_n_trials)
|
||||
live_ws = _ws_for_anchor(strategy=strat_u, out_data=data, fills=None, live=True, lg=lg)
|
||||
live_pnl = None
|
||||
@@ -527,6 +873,7 @@ def attach_topn_postprocess(
|
||||
"optuna_trial_number": None,
|
||||
"total_pnl": _krw_int(live_pnl),
|
||||
"total_trades": int((live_ob.get("orig_stats") or live_ws.get("orig_stats") or {}).get("count") or 0),
|
||||
"win_rate": (live_ob.get("orig_stats") or live_ws.get("orig_stats") or {}).get("win_rate"),
|
||||
"orderbook": live_ob,
|
||||
"whipsaw": live_ws,
|
||||
"trail": trail,
|
||||
@@ -564,7 +911,7 @@ def attach_topn_postprocess(
|
||||
"apply_overfit_factors": extra_factors,
|
||||
"apply_overfit_note": (
|
||||
"과적합%=이 숫자만 믿으면 내일 틀릴 수 있는 정도(추정). AI 아님. "
|
||||
"실매 참고행은 점수에 넣지 않음. 열 아래 적용=그 순위 누적(까지). 트레일은 별도."
|
||||
"실매 참고행은 점수에 넣지 않음. 적용=호가 8방 중 하나(또는 휩쏘/트레일 별도)."
|
||||
),
|
||||
"run_ob_whipsaw": bool(do_ob),
|
||||
}
|
||||
@@ -574,11 +921,16 @@ def attach_topn_postprocess(
|
||||
|
||||
# 하위호환: 기존 단일 키 = 합의 (apply 스크립트·웹 구표)
|
||||
if consensus.get("orderbook", {}).get("ok"):
|
||||
cnote = consensus.get("note") or "TopN 합의(gated+mode). 실매 단독 아님."
|
||||
cid = (consensus.get("combo") or {}).get("combo_id") or consensus.get("orderbook", {}).get("combo_id")
|
||||
if cid:
|
||||
cnote = f"{_COMBO_LABELS.get(str(cid), cid)} · {cnote}"
|
||||
data["orderbook_recommend"] = {
|
||||
"ok": True,
|
||||
"strategy": strat_u,
|
||||
"params": consensus["orderbook"]["params"],
|
||||
"note": "TopN 합의(gated+mode). 실매 단독 아님.",
|
||||
"combo_id": cid or "",
|
||||
"note": cnote,
|
||||
}
|
||||
if consensus.get("whipsaw", {}).get("ok"):
|
||||
data["whipsaw_recommend"] = {
|
||||
@@ -612,10 +964,21 @@ def attach_topn_postprocess(
|
||||
"📌 [후처리 TopN] anchors=%d · 과적합%%=%.1f(%s) · ob_whipsaw=%s",
|
||||
len(anchors), apply_pct, verd, do_ob,
|
||||
)
|
||||
if do_ob:
|
||||
from kis_trader.backtest import optuna_post_progress as opp
|
||||
opp.finish_job(lg)
|
||||
return data
|
||||
|
||||
|
||||
_UPTO_ORDER = ("base", "entry", "exit", "stop", "whipsaw")
|
||||
# 구 UI 누적 upto → 방 id (하위호환). whipsaw=111방 + 휩쏘 ON.
|
||||
_UPTO_TO_COMBO = {
|
||||
"base": "base",
|
||||
"entry": "e",
|
||||
"exit": "ex",
|
||||
"stop": "exs",
|
||||
"whipsaw": "exs",
|
||||
}
|
||||
_UPTO_ORDER = ("base", "entry", "exit", "stop", "whipsaw") # 레거시 별칭만
|
||||
|
||||
|
||||
def pick_postprocess_anchor(
|
||||
@@ -634,12 +997,173 @@ def pick_postprocess_anchor(
|
||||
return None
|
||||
if src == "live":
|
||||
return None
|
||||
role = "stable" if src == "stable" else "gated"
|
||||
for a in anchors:
|
||||
if str(a.get("role") or "") == "gated" and int(a.get("rank") or 0) == rk:
|
||||
if str(a.get("role") or "") == role and int(a.get("rank") or 0) == rk:
|
||||
return a
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_strat_u(strategy: str, data: Optional[Dict[str, Any]] = None) -> str:
|
||||
strat_u = str(strategy or (data or {}).get("strategy") or "").strip().upper()
|
||||
if strat_u in ("TAIL", "SHORT"):
|
||||
return "TAIL"
|
||||
if strat_u in ("SCALPING", "SCALP"):
|
||||
return "SCALP"
|
||||
if strat_u in ("US_MOMENTUM",):
|
||||
return "US_MOMENTUM"
|
||||
if strat_u in ("MOMENTUM",):
|
||||
return "MOMENTUM"
|
||||
return strat_u
|
||||
|
||||
|
||||
def _resolve_combo_id(raw: str) -> Tuple[str, bool]:
|
||||
"""반환: (combo_id, include_whipsaw)."""
|
||||
u = str(raw or "base").strip().lower()
|
||||
if u == "trail":
|
||||
return "base", False
|
||||
if u == "whipsaw":
|
||||
return "exs", True
|
||||
if u in _COMBO_IDS:
|
||||
return u, False
|
||||
if u in _UPTO_TO_COMBO:
|
||||
return _UPTO_TO_COMBO[u], False
|
||||
# 비트 표기 허용
|
||||
bit_map = {
|
||||
"000": "base", "100": "e", "010": "x", "001": "s",
|
||||
"110": "ex", "101": "es", "011": "xs", "111": "exs",
|
||||
}
|
||||
if u in bit_map:
|
||||
return bit_map[u], False
|
||||
raise ValueError(
|
||||
f"combo/upto 는 {_COMBO_IDS}+whipsaw|trail|000~111 만 (got {raw})"
|
||||
)
|
||||
|
||||
|
||||
def build_combo_env_patch(
|
||||
*,
|
||||
data: Dict[str, Any],
|
||||
source: str,
|
||||
rank: int,
|
||||
combo: str,
|
||||
strategy: str,
|
||||
include_whipsaw: Optional[bool] = None,
|
||||
) -> Tuple[Dict[str, str], List[str]]:
|
||||
"""
|
||||
8방 중 하나 적용. 켠 축만 ON·숫자 반영, 끈 축은 ENABLED=false.
|
||||
휩쏘는 8방 밖 — include_whipsaw=True 일 때만 붙임.
|
||||
"""
|
||||
from kis_trader.backtest.optuna_orderbook_recommend import (
|
||||
_env_pfx,
|
||||
build_entry_ob_env_patch,
|
||||
build_exit_ob_env_patch,
|
||||
build_stop_ob_env_patch,
|
||||
)
|
||||
from kis_trader.backtest.optuna_whipsaw_recommend import build_whipsaw_env_patch
|
||||
|
||||
cid, whip_default = _resolve_combo_id(combo)
|
||||
do_whip = bool(whip_default if include_whipsaw is None else include_whipsaw)
|
||||
notes: List[str] = []
|
||||
strat_u = _normalize_strat_u(strategy, data)
|
||||
epfx = _env_pfx(strat_u)
|
||||
use_e, use_x, use_s = _COMBO_MASK[cid]
|
||||
|
||||
anchor = pick_postprocess_anchor(data, source, rank)
|
||||
if not anchor:
|
||||
notes.append("후처리 앵커 없음(구 JSON이면 재실행 필요)")
|
||||
return {}, notes
|
||||
|
||||
ob = dict(anchor.get("orderbook") or {})
|
||||
combos = ob.get("combos") if isinstance(ob.get("combos"), dict) else {}
|
||||
c = combos.get(cid) if isinstance(combos.get(cid), dict) else None
|
||||
# 구 JSON(combos 없음): entry/exit/stop 중첩으로 폴백
|
||||
if not (c and c.get("ok")):
|
||||
legacy_map = {
|
||||
"e": ("entry",),
|
||||
"x": ("exit",),
|
||||
"s": ("stop",),
|
||||
"ex": ("exit", "entry"),
|
||||
"es": ("stop", "entry"),
|
||||
"xs": ("stop", "exit"),
|
||||
"exs": ("stop", "exit", "entry"),
|
||||
}
|
||||
if cid == "base":
|
||||
c = {"ok": True, "params": {}}
|
||||
else:
|
||||
merged_p: Dict[str, Any] = {}
|
||||
ok_any = False
|
||||
for ax in legacy_map.get(cid, ()):
|
||||
nested = ob.get(ax) if isinstance(ob.get(ax), dict) else {}
|
||||
if nested.get("ok"):
|
||||
ok_any = True
|
||||
merged_p.update(dict(nested.get("params") or {}))
|
||||
c = {"ok": ok_any, "params": merged_p} if ok_any else None
|
||||
if cid != "base" and not (c and c.get("ok")):
|
||||
notes.append(f"방 {cid} 추천 없음")
|
||||
return {}, notes
|
||||
|
||||
params = dict((c or {}).get("params") or {})
|
||||
# 마스크로 enabled 강제 (방 정의가 진실)
|
||||
params["orderbook_filter_enabled"] = bool(use_e)
|
||||
params["exit_ob_enabled"] = bool(use_x)
|
||||
params["stop_ob_enabled"] = bool(use_s)
|
||||
view = {"strategy": strat_u, "params": params, "entry": {"ok": True, "params": params},
|
||||
"exit": {"ok": True, "params": params}, "stop": {"ok": True, "params": params}}
|
||||
|
||||
patch: Dict[str, str] = {}
|
||||
if not epfx:
|
||||
notes.append("전략 prefix 없음")
|
||||
return {}, notes
|
||||
|
||||
if use_e:
|
||||
ep = build_entry_ob_env_patch(view, strat_u)
|
||||
if ep:
|
||||
patch.update(ep)
|
||||
else:
|
||||
notes.append("진입 숫자 없음 → 진입 OFF")
|
||||
patch[f"{epfx}_ORDERBOOK_FILTER_ENABLED"] = "false"
|
||||
else:
|
||||
patch[f"{epfx}_ORDERBOOK_FILTER_ENABLED"] = "false"
|
||||
|
||||
if epfx in ("MOMENTUM", "BREAKOUT"):
|
||||
if use_x:
|
||||
xp = build_exit_ob_env_patch(view, strat_u)
|
||||
if xp:
|
||||
patch.update(xp)
|
||||
else:
|
||||
notes.append("익절 숫자 없음 → 익절 OFF")
|
||||
patch[f"{epfx}_EXIT_OB_ENABLED"] = "false"
|
||||
else:
|
||||
patch[f"{epfx}_EXIT_OB_ENABLED"] = "false"
|
||||
if use_s:
|
||||
sp = build_stop_ob_env_patch(view, strat_u)
|
||||
if sp:
|
||||
patch.update(sp)
|
||||
else:
|
||||
notes.append("손절 숫자 없음 → 손절 OFF")
|
||||
patch[f"{epfx}_STOP_OB_ENABLED"] = "false"
|
||||
else:
|
||||
patch[f"{epfx}_STOP_OB_ENABLED"] = "false"
|
||||
elif use_x or use_s:
|
||||
notes.append("익절/손절호가 해당없음(전략)")
|
||||
|
||||
if do_whip:
|
||||
ws = dict(anchor.get("whipsaw") or {})
|
||||
ws["strategy"] = strat_u
|
||||
wp = build_whipsaw_env_patch(ws)
|
||||
if wp:
|
||||
patch.update(wp)
|
||||
elif strat_u in ("TAIL", "SHORT", "BREAKOUT"):
|
||||
notes.append("휩쏘 DB 스킵(전략 특성)")
|
||||
else:
|
||||
notes.append("휩쏘 추천 없음")
|
||||
elif epfx == "MOMENTUM":
|
||||
patch[f"{epfx}_WHIPSAW_FILTER_ENABLED"] = "false"
|
||||
|
||||
notes.append(f"방 {cid} 적용 (진입={int(use_e)} 익절={int(use_x)} 손절={int(use_s)} 휩쏘={int(do_whip)})")
|
||||
return patch, notes
|
||||
|
||||
|
||||
def build_upto_env_patch(
|
||||
*,
|
||||
data: Dict[str, Any],
|
||||
@@ -648,101 +1172,34 @@ def build_upto_env_patch(
|
||||
upto: str,
|
||||
strategy: str,
|
||||
) -> Tuple[Dict[str, str], List[str]]:
|
||||
"""
|
||||
누적 패치: base는 호출측 apply_params_to_db.
|
||||
여기선 entry/exit/stop/whipsaw env만.
|
||||
반환: (patch, notes)
|
||||
"""
|
||||
from kis_trader.backtest.optuna_orderbook_recommend import (
|
||||
build_entry_ob_env_patch,
|
||||
build_exit_ob_env_patch,
|
||||
build_stop_ob_env_patch,
|
||||
)
|
||||
from kis_trader.backtest.optuna_whipsaw_recommend import build_whipsaw_env_patch
|
||||
|
||||
"""하위호환: 구 누적 upto → 8방 combo 패치."""
|
||||
u = str(upto or "base").strip().lower()
|
||||
if u not in _UPTO_ORDER:
|
||||
raise ValueError(f"upto 는 {_UPTO_ORDER} 만 (got {upto})")
|
||||
notes: List[str] = []
|
||||
strat_u = str(strategy or data.get("strategy") or "").strip().upper()
|
||||
if strat_u in ("TAIL", "SHORT"):
|
||||
strat_u = "TAIL"
|
||||
elif strat_u in ("SCALPING", "SCALP"):
|
||||
strat_u = "SCALP"
|
||||
elif strat_u in ("US_MOMENTUM",):
|
||||
strat_u = "US_MOMENTUM"
|
||||
elif strat_u in ("MOMENTUM",):
|
||||
strat_u = "MOMENTUM"
|
||||
|
||||
anchor = pick_postprocess_anchor(data, source, rank)
|
||||
if not anchor:
|
||||
notes.append("후처리 앵커 없음(구 JSON이면 재실행 필요)")
|
||||
return {}, notes
|
||||
ob = dict(anchor.get("orderbook") or {})
|
||||
ob["strategy"] = strat_u
|
||||
ws = dict(anchor.get("whipsaw") or {})
|
||||
ws["strategy"] = strat_u
|
||||
patch: Dict[str, str] = {}
|
||||
idx = _UPTO_ORDER.index(u)
|
||||
if u == "base":
|
||||
patch.update(_later_axes_off_patch(strat_u, u))
|
||||
notes.append("뒤쪽 후처리축 ENABLED=false (숫자는 유지)")
|
||||
return patch, notes
|
||||
if idx >= _UPTO_ORDER.index("entry"):
|
||||
ep = build_entry_ob_env_patch(ob, strat_u)
|
||||
if ep:
|
||||
patch.update(ep)
|
||||
else:
|
||||
notes.append("진입호가 추천 없음")
|
||||
if idx >= _UPTO_ORDER.index("exit"):
|
||||
xp = build_exit_ob_env_patch(ob, strat_u)
|
||||
if xp:
|
||||
patch.update(xp)
|
||||
elif strat_u not in ("MOMENTUM", "BREAKOUT"):
|
||||
notes.append("익절호가 해당없음")
|
||||
else:
|
||||
notes.append("익절호가 추천 없음")
|
||||
if idx >= _UPTO_ORDER.index("stop"):
|
||||
sp = build_stop_ob_env_patch(ob, strat_u)
|
||||
if sp:
|
||||
patch.update(sp)
|
||||
elif strat_u not in ("MOMENTUM", "BREAKOUT"):
|
||||
notes.append("손절호가 해당없음")
|
||||
else:
|
||||
notes.append("손절호가 추천 없음")
|
||||
if idx >= _UPTO_ORDER.index("whipsaw"):
|
||||
wp = build_whipsaw_env_patch(ws)
|
||||
if wp:
|
||||
patch.update(wp)
|
||||
elif strat_u in ("TAIL", "SHORT", "BREAKOUT"):
|
||||
notes.append("휩쏘 DB 스킵(전략 특성)")
|
||||
else:
|
||||
notes.append("휩쏘 추천 없음")
|
||||
off = _later_axes_off_patch(strat_u, u)
|
||||
patch.update(off)
|
||||
if off:
|
||||
notes.append("선택 열 이후 축 ENABLED=false (숫자는 유지)")
|
||||
return patch, notes
|
||||
if u == "trail":
|
||||
return {}, ["trail은 다단트레일 전용 경로"]
|
||||
return build_combo_env_patch(
|
||||
data=data, source=source, rank=rank, combo=u, strategy=strategy,
|
||||
)
|
||||
|
||||
|
||||
def _later_axes_off_patch(strat_u: str, upto: str) -> Dict[str, str]:
|
||||
"""선택 열 이후는 끄기만. 스프레드·OR·dip 숫자는 안 지움."""
|
||||
"""레거시 누적 off. 신규는 build_combo_env_patch 마스크 사용."""
|
||||
from kis_trader.backtest.optuna_orderbook_recommend import _env_pfx
|
||||
|
||||
pfx = _env_pfx(strat_u)
|
||||
if not pfx:
|
||||
return {}
|
||||
u = str(upto or "base").strip().lower()
|
||||
if u not in _UPTO_ORDER:
|
||||
try:
|
||||
cid, do_whip = _resolve_combo_id(upto)
|
||||
except ValueError:
|
||||
return {}
|
||||
idx = _UPTO_ORDER.index(u)
|
||||
use_e, use_x, use_s = _COMBO_MASK[cid]
|
||||
patch: Dict[str, str] = {}
|
||||
if idx < _UPTO_ORDER.index("entry"):
|
||||
if not use_e:
|
||||
patch[f"{pfx}_ORDERBOOK_FILTER_ENABLED"] = "false"
|
||||
if idx < _UPTO_ORDER.index("exit") and pfx in ("MOMENTUM", "BREAKOUT"):
|
||||
if not use_x and pfx in ("MOMENTUM", "BREAKOUT"):
|
||||
patch[f"{pfx}_EXIT_OB_ENABLED"] = "false"
|
||||
if idx < _UPTO_ORDER.index("stop") and pfx in ("MOMENTUM", "BREAKOUT"):
|
||||
if not use_s and pfx in ("MOMENTUM", "BREAKOUT"):
|
||||
patch[f"{pfx}_STOP_OB_ENABLED"] = "false"
|
||||
if idx < _UPTO_ORDER.index("whipsaw") and pfx == "MOMENTUM":
|
||||
if (not do_whip) and pfx == "MOMENTUM":
|
||||
patch[f"{pfx}_WHIPSAW_FILTER_ENABLED"] = "false"
|
||||
return patch
|
||||
|
||||
Reference in New Issue
Block a user