거래 빠르게 안티에서 병신만든거 커서로
feat: Implement backtest source management and enhance candle data handling Changes: - Introduced a new function `_apply_backtest_source_env_from_request` to manage the environment variables for candle, tick, and order book sources based on incoming requests. - Added a teardown function `_teardown_backtest_source_env` to ensure that environment variables do not persist between requests, enhancing the stability of the backtesting environment. - Refactored existing code to utilize the new source management functions, improving code readability and maintainability. - Added new utility functions in `bt_candle_source.py` for fetching and managing candle data, ensuring consistency with live trading data sources. Impact: - These changes improve the flexibility and reliability of the backtesting framework, allowing for better management of data sources and reducing the risk of cross-request contamination.
This commit is contained in:
@@ -79,22 +79,97 @@ def _parse_ratchet_tiers(val: str) -> List[Tuple[int, float]]:
|
||||
return t
|
||||
|
||||
|
||||
def resolve_orderbook_recommend_table(
|
||||
*,
|
||||
ob_table: Optional[str] = None,
|
||||
history_source: Optional[str] = None,
|
||||
ob_source: Optional[str] = None,
|
||||
) -> Tuple[str, Tuple[str, ...]]:
|
||||
"""후처리 호가 테이블 — 전략명 하드코딩 금지.
|
||||
|
||||
우선순위 (백테 ``trigger_snapshot_loader`` / ``OB_SOURCE`` 와 동일 축):
|
||||
1) 명시 ``ob_table``
|
||||
2) ``ob_source`` 또는 env ``OB_SOURCE`` (kis|kiwoom|kiwoom_0d|ls)
|
||||
3) ``history_source`` 또는 ``BACKTEST_UNIVERSE_HISTORY_SOURCE`` (ls → ls_ws_orderbook)
|
||||
|
||||
Returns:
|
||||
(table_name, source_filter) — source_filter 비어 있으면 source 조건 없음.
|
||||
"""
|
||||
import os
|
||||
|
||||
if ob_table and str(ob_table).strip():
|
||||
t = str(ob_table).strip()
|
||||
if t == "kis_ws_orderbook":
|
||||
return t, tuple()
|
||||
if t == "ls_ws_orderbook":
|
||||
return t, ("ls_uh1", "ls_h1", "ls_ha", "ls_nh1")
|
||||
return t, ("kiwoom_0d",)
|
||||
|
||||
raw_ob = (ob_source if ob_source is not None else os.environ.get("OB_SOURCE", "")).strip().lower()
|
||||
if raw_ob in ("kis", "kis_ws"):
|
||||
return "kis_ws_orderbook", tuple()
|
||||
if raw_ob in ("ls", "ls_condition", "ls_ws", "ls_afr"):
|
||||
return "ls_ws_orderbook", ("ls_uh1", "ls_h1", "ls_ha", "ls_nh1")
|
||||
if raw_ob in ("kiwoom", "kiwoom_0d", "0d"):
|
||||
return "ws_orderbook", ("kiwoom_0d",)
|
||||
|
||||
try:
|
||||
from kis_trader.backtest.universe_history_source import (
|
||||
resolve_backtest_universe_history_source,
|
||||
)
|
||||
|
||||
hs = resolve_backtest_universe_history_source(history_source)
|
||||
except Exception:
|
||||
hs = str(history_source or "kiwoom").strip().lower()
|
||||
if hs in ("ls", "ls_condition", "ls_afr", "ls_ws"):
|
||||
hs = "ls"
|
||||
else:
|
||||
hs = "kiwoom"
|
||||
|
||||
if hs == "ls":
|
||||
return "ls_ws_orderbook", ("ls_uh1", "ls_h1", "ls_ha", "ls_nh1")
|
||||
# 기본(키움 이력) — 실수집 본체
|
||||
return "ws_orderbook", ("kiwoom_0d",)
|
||||
|
||||
|
||||
def get_orderbook_table_for_strategy(strategy: str) -> str:
|
||||
s = strategy.upper()
|
||||
if any(p in s for p in ("BREAKOUT", "SCALP", "LS")):
|
||||
return "ls_ws_orderbook"
|
||||
return "ws_orderbook"
|
||||
"""호환용 — 전략명으로 LS 강제하지 않음. history/ob_source 해석."""
|
||||
table, _src = resolve_orderbook_recommend_table()
|
||||
return table
|
||||
|
||||
|
||||
def _strategy_config_table_and_prefix(strat_upper: str) -> Tuple[str, str]:
|
||||
s = (strat_upper or "").strip().upper()
|
||||
if "BREAKOUT" in s:
|
||||
return "config_breakout", "BREAKOUT_"
|
||||
if "SCALP" in s:
|
||||
return "config_scalp", "SCALP_"
|
||||
if s in ("SHORT", "TAIL") or "TAIL" in s:
|
||||
return "config_short", "TAIL_"
|
||||
if "US_MOMENTUM" in s or s.startswith("US"):
|
||||
return "config_us_momentum", "US_MOMENTUM_"
|
||||
return "config_momentum", "MOMENTUM_"
|
||||
|
||||
|
||||
def recommend_orderbook_parameters(
|
||||
strategy: str = "MOMENTUM",
|
||||
n_trials: int = 1000,
|
||||
ob_table: Optional[str] = None,
|
||||
history_source: Optional[str] = None,
|
||||
ob_source: Optional[str] = None,
|
||||
log: Optional[logging.Logger] = None,
|
||||
) -> Dict[str, Any]:
|
||||
lg = log or logger
|
||||
strat_upper = strategy.upper()
|
||||
table = ob_table or get_orderbook_table_for_strategy(strat_upper)
|
||||
table, source_filter = resolve_orderbook_recommend_table(
|
||||
ob_table=ob_table,
|
||||
history_source=history_source,
|
||||
ob_source=ob_source,
|
||||
)
|
||||
lg.info(
|
||||
"📌 [호가 후처리] strategy=%s table=%s source_filter=%s",
|
||||
strat_upper, table, source_filter or "(all)",
|
||||
)
|
||||
|
||||
db = TradeDB()
|
||||
# 1. 테이블 존재 여부 및 컬럼 검사
|
||||
@@ -103,19 +178,39 @@ def recommend_orderbook_parameters(
|
||||
need = {"code", "snap_time", "total_bid_qty", "total_ask_qty", "best_bid", "best_ask"}
|
||||
if need - set(cols):
|
||||
lg.warning("⚠️ [%s] 호가 테이블 필수 컬럼 부족. 추천 생략.", table)
|
||||
return {"ok": False, "reason": "insufficient_columns"}
|
||||
return {"ok": False, "reason": "insufficient_columns", "table": table}
|
||||
except Exception as exc:
|
||||
lg.warning("⚠️ [%s] 테이블 조회 실패: %s. 추천 생략.", table, exc)
|
||||
return {"ok": False, "reason": "table_not_found"}
|
||||
return {"ok": False, "reason": "table_not_found", "table": table}
|
||||
|
||||
date_rows = db.conn.execute(f"SELECT DISTINCT SUBSTR(snap_time, 1, 8) as dt FROM {table} ORDER BY dt").fetchall()
|
||||
date_sql = f"SELECT DISTINCT SUBSTR(snap_time, 1, 8) as dt FROM {table}"
|
||||
date_params: Tuple[Any, ...] = ()
|
||||
if source_filter and "source" in cols:
|
||||
ph = ",".join(["%s"] * len(source_filter))
|
||||
date_sql += f" WHERE source IN ({ph})"
|
||||
date_params = tuple(source_filter)
|
||||
date_sql += " ORDER BY dt"
|
||||
date_rows = db.conn.execute(date_sql, date_params).fetchall()
|
||||
avail_dates = [str(r["dt"]) for r in date_rows if r["dt"] and str(r["dt"]) != "None"]
|
||||
# source 필터에 안 걸린 구행만 있을 때 — 필터 없이 1회 재시도
|
||||
if not avail_dates and source_filter and "source" in cols:
|
||||
date_rows = db.conn.execute(
|
||||
f"SELECT DISTINCT SUBSTR(snap_time, 1, 8) as dt FROM {table} ORDER BY dt"
|
||||
).fetchall()
|
||||
avail_dates = [str(r["dt"]) for r in date_rows if r["dt"] and str(r["dt"]) != "None"]
|
||||
if avail_dates:
|
||||
source_filter = tuple()
|
||||
lg.info("📌 [호가 후처리] source 필터 미스 → 전체 source 사용 table=%s", table)
|
||||
if not avail_dates:
|
||||
return {"ok": False, "reason": "no_orderbook_snapshots"}
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "no_orderbook_snapshots",
|
||||
"table": table,
|
||||
"source_filter": list(source_filter),
|
||||
}
|
||||
|
||||
# 2. 전략별 config (래칫/어깨) 로딩
|
||||
cfg_table = "config_breakout" if "BREAKOUT" in strat_upper else "config_momentum"
|
||||
pfx = "BREAKOUT_" if "BREAKOUT" in strat_upper else "MOMENTUM_"
|
||||
cfg_table, pfx = _strategy_config_table_and_prefix(strat_upper)
|
||||
try:
|
||||
row_cfg = db.conn.execute(
|
||||
f"SELECT {pfx}RATCHET_TIERS, {pfx}SHOULDER_MIN_HIGH_PCT, {pfx}SHOULDER_CUT_PCT FROM {cfg_table} ORDER BY id DESC LIMIT 1"
|
||||
@@ -159,11 +254,21 @@ def recommend_orderbook_parameters(
|
||||
lookback = timedelta(minutes=10)
|
||||
horizon = timedelta(minutes=6)
|
||||
|
||||
s_rows = db.conn.execute(
|
||||
snap_sql = (
|
||||
f"SELECT snap_time, total_bid_qty, total_ask_qty, best_bid, best_ask FROM {table} "
|
||||
"WHERE code=%s AND snap_time >= %s AND snap_time < %s ORDER BY snap_time ASC",
|
||||
(code, (buy_dt - lookback).strftime("%Y%m%d%H%M%S"), (buy_dt + horizon).strftime("%Y%m%d%H%M%S")),
|
||||
).fetchall()
|
||||
"WHERE code=%s AND snap_time >= %s AND snap_time < %s"
|
||||
)
|
||||
snap_params: List[Any] = [
|
||||
code,
|
||||
(buy_dt - lookback).strftime("%Y%m%d%H%M%S"),
|
||||
(buy_dt + horizon).strftime("%Y%m%d%H%M%S"),
|
||||
]
|
||||
if source_filter and "source" in cols:
|
||||
ph = ",".join(["%s"] * len(source_filter))
|
||||
snap_sql += f" AND source IN ({ph})"
|
||||
snap_params.extend(source_filter)
|
||||
snap_sql += " ORDER BY snap_time ASC"
|
||||
s_rows = db.conn.execute(snap_sql, tuple(snap_params)).fetchall()
|
||||
snaps: List[Snap] = []
|
||||
for sr in s_rows:
|
||||
s_dt = _snap_to_dt(sr["snap_time"])
|
||||
@@ -390,13 +495,29 @@ def attach_orderbook_recommend(
|
||||
"""out_data에 호가 진입/청산 합의 수치 추천(orderbook_recommend)을 첨부."""
|
||||
lg = log or logger
|
||||
strat = str(out_data.get("strategy") or "MOMENTUM").strip().upper()
|
||||
rec = recommend_orderbook_parameters(strategy=strat, n_trials=1000, log=lg)
|
||||
hist = (
|
||||
out_data.get("universe_history_source")
|
||||
or out_data.get("_universe_history_source")
|
||||
or out_data.get("history_source")
|
||||
)
|
||||
ob_src = out_data.get("ob_source") or out_data.get("orderbook_source")
|
||||
rec = recommend_orderbook_parameters(
|
||||
strategy=strat,
|
||||
n_trials=1000,
|
||||
history_source=hist,
|
||||
ob_source=ob_src,
|
||||
log=lg,
|
||||
)
|
||||
out_data["orderbook_recommend"] = rec
|
||||
mc = out_data.get("mode_combo")
|
||||
if isinstance(mc, dict):
|
||||
mc["orderbook_recommend"] = rec
|
||||
if not rec.get("ok"):
|
||||
lg.info("⚡ [호가 수급 합의 추천] 생략 — %s", rec.get("reason") or "n/a")
|
||||
lg.info(
|
||||
"⚡ [호가 수급 합의 추천] 생략 — %s (table=%s)",
|
||||
rec.get("reason") or "n/a",
|
||||
rec.get("table") or "?",
|
||||
)
|
||||
return out_data
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user