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

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

@@ -43,6 +43,17 @@ class TailCatchStrategy(BaseStrategy):
# ------------------------------------------------------------------
def reload_config(self) -> None:
# 루프마다 호출 — 병목 시 [RELOAD_PROF] 로 단계 ms 기록
_rp_t0 = time.perf_counter()
_rp_last = _rp_t0
_rp: Dict[str, float] = {}
def _rp_mark(stage: str) -> None:
nonlocal _rp_last
now = time.perf_counter()
_rp[stage] = (now - _rp_last) * 1000.0
_rp_last = now
self.min_price = get_env_float("MIN_STOCK_PRICE", 1000.0)
self.stop_loss_pct = get_env_float("STOP_LOSS_PCT", -0.04)
self.take_profit_pct = get_env_float("TAKE_PROFIT_PCT", 0.05)
@@ -50,9 +61,13 @@ class TailCatchStrategy(BaseStrategy):
get_env_int("TAIL_SLOT_MONEY", 0)
or get_env_int("SLOT_MONEY_DEFAULT", 3_000_000)
)
_rp_mark("env_basic")
if te is not None:
try:
# ※ get_tail_defaults_from_db → db.get_merged_env_snapshot() 직접
# (SCALP 는 get_strategy_env_dict RAM 캐시 경로 · SHORT/MOM 은 매 루프 DB)
p = te.get_tail_defaults_from_db(self.db)
_rp_mark("tail_defaults")
p["live_backtest_align"] = get_env_bool(
"SHORT_LIVE_BACKTEST_ALIGN", True,
)
@@ -61,10 +76,38 @@ class TailCatchStrategy(BaseStrategy):
)
p["entry_mode"] = short_entry_mode()
self._engine_params = p
_rp_mark("tail_flags")
except Exception as e:
self.logger.debug("tail_engine defaults 조회 실패: %s", e)
_rp_mark("tail_err")
self.eod_enabled = get_env_bool("TAIL_EOD_ENABLED", True)
self.eod_hm = get_env_from_db("TAIL_EOD_HM", "15:20")
_rp_mark("eod")
_rp_total = (time.perf_counter() - _rp_t0) * 1000.0
# 500ms 이상만 — 장중 수 초 reload 원인 확정용
if _rp_total >= 500.0:
parts = [f"[RELOAD_PROF] SHORT total={_rp_total:.1f}"]
for k, v in sorted(_rp.items(), key=lambda x: -x[1]):
parts.append(f"{k}={v:.1f}")
line = " ".join(parts)
try:
self.logger.info("%s", line)
except Exception:
pass
try:
path = str(get_env_from_db("LOOP_PROFILE_LOG_PATH", "logs/loop_profile.log") or "").strip()
if path:
import os
from datetime import datetime as _dt
if not os.path.isabs(path):
root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
path = os.path.join(root, path)
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "a", encoding="utf-8") as f:
f.write(_dt.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + " " + line + "\n")
except Exception:
pass
def _candidate_filter(self, candidate: Dict) -> bool:
"""tail_on 이 True 인 후보만 대상 (SCALP 과 분리)."""
@@ -151,19 +194,23 @@ class TailCatchStrategy(BaseStrategy):
if self.is_dip_buy_excluded(code):
self.logger.info("🔍 [탈락-대형주제외] %s %s: DIP_BUY_EXCLUDE_CODES", name, code)
return None
_cb = self._cb_prof_start(code)
try:
if get_env_bool("FORCE_BUY_TEST", False):
return self._force_buy_test(code, name)
min_len = get_env_int("MIN_CANDLE_LEN_TAIL", 14)
candles_raw = self.ws.get_candles(code, self.candle_tf, n=50)
self._cb_prof_mark(_cb, "candles")
if len(candles_raw) < min_len:
try:
self.ws.fill_gap([code], force=True)
except Exception:
pass
self._cb_prof_mark(_cb, "fill_gap")
return None
candles = [self._norm_candle(c) for c in candles_raw]
self._cb_prof_mark(_cb, "norm")
if len(candles) < 10:
return None
@@ -177,7 +224,7 @@ class TailCatchStrategy(BaseStrategy):
except Exception:
pass
try:
today_trades = self.db.get_trades_by_date(today)
today_trades = self._get_today_trades(today)
code_trades = [
t for t in today_trades
if t.get("code") == code and str(t.get("strategy", "")).startswith("SHORT")
@@ -189,6 +236,7 @@ class TailCatchStrategy(BaseStrategy):
except Exception:
daily_cnt = 0
daily_pnl_krw = 0.0
self._cb_prof_mark(_cb, "trades_db")
state = {
"last_exit_dt": last_exit_dt,
"daily_cnt": daily_cnt,
@@ -204,6 +252,7 @@ class TailCatchStrategy(BaseStrategy):
params["_program_code"] = code
params["slot_money"] = self.slot_money
reject, msg, sig = te.check_buy_signal_live(candles, params, state)
self._cb_prof_mark(_cb, "engine")
if reject:
self.logger.info("🔍 [%s] %s %s: %s", reject, name, code, msg or "")
return None
@@ -217,6 +266,7 @@ class TailCatchStrategy(BaseStrategy):
eng = params if params else te.get_tail_defaults_from_db(self.db)
atr_period = int(eng.get("atr_period", 14))
atr_series = te.compute_atr_series(candles, atr_period)
self._cb_prof_mark(_cb, "atr")
if is_limit_atr_entry(short_entry_mode(eng)):
if len(candles) < 2:
@@ -246,6 +296,7 @@ class TailCatchStrategy(BaseStrategy):
qty, rej = self._resolve_buy_qty_live(
float(limit_int), hard_cap=hard_cap,
)
self._cb_prof_mark(_cb, "qty")
if rej:
self.logger.info(
"🔍 [탈락-%s] %s(%s) limit=%s",
@@ -285,6 +336,7 @@ class TailCatchStrategy(BaseStrategy):
_defer = self._defer_mid_enroll_entry(
code, _ebk, int(self.candle_tf or 3),
)
self._cb_prof_mark(_cb, "mid_enroll")
if _defer:
self.logger.info("🔍 [%s] %s(%s)", _defer, name, code)
return None
@@ -307,6 +359,7 @@ class TailCatchStrategy(BaseStrategy):
) or curr_price
except Exception:
pass
self._cb_prof_mark(_cb, "align")
if curr_price <= 0 or curr_price < self.min_price:
return None
@@ -316,6 +369,7 @@ class TailCatchStrategy(BaseStrategy):
qty, rej = self._resolve_buy_qty_live(
curr_price, hard_cap=hard_cap,
)
self._cb_prof_mark(_cb, "qty")
if rej:
self.logger.info(
"🔍 [탈락-%s] %s(%s) price=%.0f",
@@ -369,6 +423,8 @@ class TailCatchStrategy(BaseStrategy):
except Exception as e:
self.logger.info("🔍 [탈락-예외] %s %s: %s", name, code, e)
return None
finally:
self._cb_prof_finish(_cb)
def _force_buy_test(self, code: str, name: str) -> Optional[Dict]:
wsd = self.ws.get_price(code)