거래 빠르게 안티에서 병신만든거 커서로

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:
Your Name
2026-08-13 16:03:40 +09:00
parent c6bd62a25f
commit 2c7ad867f4
53 changed files with 15251 additions and 637 deletions

View File

@@ -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