feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
import kis_trader.engine.tail_engine as te
|
||||
from kis_trader.backtest.backtest_portfolio_common import resolve_trigger_snapshots_for_backtest
|
||||
@@ -18,6 +19,9 @@ from kis_trader.engine.indicator_cache import (
|
||||
TAIL_STRATEGY_ID = "SHORT"
|
||||
VALID_TIMEFRAMES = (3, 5, 15, 60)
|
||||
|
||||
# 종목×기간일 REST 웜업 캐시 (프로세스 메모리만 — DB 미기록)
|
||||
_TAIL_REST_WARMUP_PREFIX_CACHE: Dict[Tuple[str, str, int], List[Dict[str, Any]]] = {}
|
||||
|
||||
|
||||
def tail_backtest_universe_scan_at_enabled(params: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""백테 유니버스: 1분 슬롯 대신 초단위 스캔시각 타임라인 (기본 ON, 실매 정합).
|
||||
@@ -44,6 +48,236 @@ def date_keys(start: str, end: str) -> Tuple[str, str, str, str]:
|
||||
return start_key, end_key, start_key[:8], end_key[:8]
|
||||
|
||||
|
||||
def tail_backtest_candle_warmup_bars() -> int:
|
||||
"""백테 지표 warm-up — 실매 ``get_candles(..., n=50)`` 과 동일하게 전일 N봉(전략 TF).
|
||||
|
||||
꼬리 기본 TF=3분 → 50봉 ≈ 실매 RAM 조회 길이. RSI·패턴은 확정봉 ≥20 필요.
|
||||
"""
|
||||
from kis_trader.utils.env import get_env_int
|
||||
return max(0, int(get_env_int("TAIL_BACKTEST_CANDLE_WARMUP_BARS", 50)))
|
||||
|
||||
|
||||
def _tail_rows_have_prev_day(rows: List[Dict], period_day: str) -> bool:
|
||||
pd = str(period_day or "")[:8]
|
||||
if not pd:
|
||||
return True
|
||||
for r in rows or []:
|
||||
ct = str(r.get("candle_time") or "")
|
||||
if len(ct) >= 8 and ct[:8] < pd:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def prepend_tail_candle_warmup(
|
||||
db,
|
||||
candles_by_code: Dict[str, List[Dict]],
|
||||
period_start_key: str,
|
||||
*,
|
||||
timeframe: int = 3,
|
||||
warmup_bars: Optional[int] = None,
|
||||
peak_sel: str = "",
|
||||
) -> int:
|
||||
"""
|
||||
``period_start_key`` 이전 N봉(전략 TF)을 종목별로 prepend.
|
||||
지표·패턴용 — 엔진 ``all_times`` 는 기간일만 쓰도록 ``_bt_period_start_ymd`` 로 걸러짐.
|
||||
"""
|
||||
wb = (
|
||||
tail_backtest_candle_warmup_bars()
|
||||
if warmup_bars is None
|
||||
else max(0, int(warmup_bars))
|
||||
)
|
||||
if wb <= 0 or db is None or not period_start_key:
|
||||
return 0
|
||||
tf = int(timeframe)
|
||||
if tf not in VALID_TIMEFRAMES:
|
||||
tf = 3
|
||||
ps = str(period_start_key)[:12]
|
||||
ind_cols = ws_candles_select_indicator_cols(db)
|
||||
total_prepended = 0
|
||||
for code, rows in list(candles_by_code.items()):
|
||||
if not rows:
|
||||
continue
|
||||
# 이미 기간 이전 봉이 있으면 skip
|
||||
if _tail_rows_have_prev_day(rows, ps[:8]):
|
||||
continue
|
||||
first_ct = ""
|
||||
for r in rows:
|
||||
ct = str(r.get("candle_time") or "")
|
||||
if ct >= ps:
|
||||
first_ct = ct
|
||||
break
|
||||
if not first_ct:
|
||||
continue
|
||||
warm_rows = db.conn.execute(
|
||||
f"SELECT candle_time, open, high, low, close, volume{peak_sel}{ind_cols} "
|
||||
"FROM ws_candles WHERE timeframe=%s AND code=%s "
|
||||
"AND candle_time < %s AND is_confirmed=1 "
|
||||
"ORDER BY candle_time DESC LIMIT %s",
|
||||
[tf, code, first_ct, wb],
|
||||
).fetchall()
|
||||
if not warm_rows:
|
||||
continue
|
||||
prefix = [dict(r) for r in reversed(warm_rows)]
|
||||
candles_by_code[code] = prefix + [dict(r) for r in rows]
|
||||
total_prepended += len(prefix)
|
||||
if total_prepended > 0:
|
||||
materialize_ws_candles_batch(db, candles_by_code, tf)
|
||||
return total_prepended
|
||||
|
||||
|
||||
def inject_tail_rest_warmup_memory(
|
||||
candles_by_code: Dict[str, List[Dict]],
|
||||
period_start_key: str,
|
||||
*,
|
||||
timeframe: int = 3,
|
||||
universe_by_slot: Optional[Dict[str, List[str]]] = None,
|
||||
) -> Dict[str, int]:
|
||||
"""
|
||||
DB 전일봉이 없을 때 키움 ka10080 REST 1회 → 1분봉 → TF 롤업 → 메모리 prepend.
|
||||
|
||||
- 실매: ``SHORT_GAP_FILL_LIMIT``(기본 150) 1M/3M 갭보정
|
||||
- 백테: ``TAIL_BACKTEST_REST_WARMUP_BARS`` 기본 **150**(1분) → 3분 ≈50봉
|
||||
(= 실매 ``get_candles(n=50)`` 근사). 모멘텀 REST 기본 700(1분·HTS E)과 다름.
|
||||
- DB INSERT 없음.
|
||||
"""
|
||||
from kis_trader.utils.env import get_env_bool, get_env_float, get_env_int
|
||||
from kis_trader.utils.logger import get_logger
|
||||
from kis_trader.engine.candle_rollup import rollup_1m_bars_to_tf
|
||||
|
||||
log = get_logger("kis_trader.backtest.tail")
|
||||
stats = {"need": 0, "ok": 0, "fail": 0, "cache_hit": 0, "bars": 0, "skipped": 0}
|
||||
if not get_env_bool("TAIL_BACKTEST_REST_WARMUP", True):
|
||||
stats["skipped"] = 1
|
||||
return stats
|
||||
ps = str(period_start_key or "")[:12]
|
||||
if len(ps) < 8 or not candles_by_code:
|
||||
return stats
|
||||
period_day = ps[:8]
|
||||
tf = int(timeframe) if int(timeframe) in VALID_TIMEFRAMES else 3
|
||||
|
||||
if universe_by_slot:
|
||||
target: Set[str] = set()
|
||||
for codes in universe_by_slot.values():
|
||||
for c in codes or []:
|
||||
if c:
|
||||
target.add(str(c).strip())
|
||||
target &= set(candles_by_code.keys())
|
||||
else:
|
||||
target = set(candles_by_code.keys())
|
||||
|
||||
need_codes = [
|
||||
c for c in sorted(target)
|
||||
if not _tail_rows_have_prev_day(candles_by_code.get(c) or [], period_day)
|
||||
]
|
||||
stats["need"] = len(need_codes)
|
||||
if not need_codes:
|
||||
return stats
|
||||
|
||||
max_codes = int(get_env_int("TAIL_BACKTEST_REST_MAX_CODES", 0))
|
||||
if max_codes > 0:
|
||||
need_codes = need_codes[:max_codes]
|
||||
|
||||
# 1분봉 개수 — 전일 세션 커버용 (장중 1분≈380봉). 기본 450.
|
||||
# (150은 당일 최근만 닿아 prev_day 판정 실패 → REST 무의미. 모멘텀 700보다 작고
|
||||
# SHORT_GAP_FILL_LIMIT(150·3분)≈450·1분 과 맞춤.)
|
||||
n_1m = max(
|
||||
50,
|
||||
int(get_env_int("TAIL_BACKTEST_REST_WARMUP_BARS", 450)),
|
||||
)
|
||||
sleep_sec = float(get_env_float("TAIL_BACKTEST_REST_SLEEP_SEC", 0.25))
|
||||
|
||||
from kis_trader.backtest.momentum_backtest_common import _kiwoom_gap_credentials
|
||||
from kis_trader.ws.kis_ws import get_kiwoom_candles_df
|
||||
|
||||
kw_key, kw_secret, is_mock = _kiwoom_gap_credentials()
|
||||
if not kw_key or not kw_secret:
|
||||
log.warning("⚠️ 꼬리 REST 웜업 스킵 — 키움 앱키/시크릿 없음")
|
||||
stats["fail"] = len(need_codes)
|
||||
return stats
|
||||
|
||||
log.info(
|
||||
"📡 꼬리 REST 웜업(메모리): 전일봉 부족 %d종목 · 1M n=%d → %dM 롤업 (DB 미기록)",
|
||||
len(need_codes), n_1m, tf,
|
||||
)
|
||||
for i, code in enumerate(need_codes):
|
||||
rows = candles_by_code.get(code) or []
|
||||
if not rows:
|
||||
stats["fail"] += 1
|
||||
continue
|
||||
cache_key = (code, period_day, tf)
|
||||
cached = _TAIL_REST_WARMUP_PREFIX_CACHE.get(cache_key)
|
||||
if cached is not None:
|
||||
stats["cache_hit"] += 1
|
||||
prefix = [dict(r) for r in cached]
|
||||
else:
|
||||
try:
|
||||
df = get_kiwoom_candles_df(
|
||||
code, 1, kw_key, kw_secret, is_mock=is_mock, n=n_1m,
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning("⚠️ 꼬리 REST 웜업 실패 %s: %s", code, e)
|
||||
stats["fail"] += 1
|
||||
continue
|
||||
if df is None or getattr(df, "empty", True):
|
||||
stats["fail"] += 1
|
||||
continue
|
||||
first_ct = ""
|
||||
for r in rows:
|
||||
ct = str(r.get("candle_time") or "")
|
||||
if ct >= ps:
|
||||
first_ct = ct[:12]
|
||||
break
|
||||
if not first_ct:
|
||||
first_ct = ps
|
||||
existing = {str(r.get("candle_time") or "")[:12] for r in rows}
|
||||
bars_1m: List[Dict[str, Any]] = []
|
||||
try:
|
||||
for _, rec in df.iterrows():
|
||||
t = str(rec.get("time") or "")[:12]
|
||||
if len(t) < 12 or t >= first_ct:
|
||||
continue
|
||||
op = float(rec.get("open") or 0)
|
||||
if op <= 0:
|
||||
continue
|
||||
bars_1m.append({
|
||||
"candle_time": t,
|
||||
"open": op,
|
||||
"high": float(rec.get("high") or op),
|
||||
"low": float(rec.get("low") or op),
|
||||
"close": float(rec.get("close") or op),
|
||||
"volume": int(float(rec.get("volume") or 0)),
|
||||
"is_confirmed": 1,
|
||||
"_rest_warmup": 1,
|
||||
})
|
||||
except Exception as e:
|
||||
log.warning("⚠️ 꼬리 REST 웜업 파싱 실패 %s: %s", code, e)
|
||||
stats["fail"] += 1
|
||||
continue
|
||||
bars_1m.sort(key=lambda x: str(x.get("candle_time") or ""))
|
||||
rolled = rollup_1m_bars_to_tf(bars_1m, tf) if tf != 1 else bars_1m
|
||||
prefix = [
|
||||
dict(r) for r in rolled
|
||||
if str(r.get("candle_time") or "")[:12] < first_ct
|
||||
and str(r.get("candle_time") or "")[:12] not in existing
|
||||
]
|
||||
_TAIL_REST_WARMUP_PREFIX_CACHE[cache_key] = [dict(r) for r in prefix]
|
||||
if sleep_sec > 0 and i + 1 < len(need_codes):
|
||||
time.sleep(sleep_sec)
|
||||
|
||||
if not prefix or not _tail_rows_have_prev_day(prefix, period_day):
|
||||
stats["fail"] += 1
|
||||
continue
|
||||
candles_by_code[code] = [dict(r) for r in prefix] + [dict(r) for r in rows]
|
||||
stats["ok"] += 1
|
||||
stats["bars"] += len(prefix)
|
||||
|
||||
log.info(
|
||||
"✅ 꼬리 REST 웜업 완료: ok=%d fail=%d cache=%d bars=%d",
|
||||
stats["ok"], stats["fail"], stats["cache_hit"], stats["bars"],
|
||||
)
|
||||
return stats
|
||||
|
||||
|
||||
def resolve_tail_universe(
|
||||
start_ymd: str,
|
||||
end_ymd: str,
|
||||
@@ -83,7 +317,12 @@ def load_tail_candles_by_code(
|
||||
"""
|
||||
ws_candles 전 종목 로드 (backtest_web api/backtest/tail 과 동일 쿼리).
|
||||
유니버스 필터는 엔진 run_tail_backtest 에서 슬롯별 적용.
|
||||
|
||||
``TAIL_BT_SYNTH_3M_FROM_1M``(기본 true) 이고 timeframe=3 이면
|
||||
DB 3분 구멍을 1분봉 롤업으로 보강 (실매 RAM 롤업과 동일 규칙).
|
||||
"""
|
||||
from kis_trader.utils.env import get_env_bool
|
||||
|
||||
tail_tf = int(timeframe)
|
||||
if tail_tf not in VALID_TIMEFRAMES:
|
||||
raise ValueError(f"timeframe 은 {VALID_TIMEFRAMES} 중 하나여야 합니다")
|
||||
@@ -104,9 +343,30 @@ def load_tail_candles_by_code(
|
||||
).fetchall()
|
||||
codes = [r["code"] for r in codes_raw]
|
||||
|
||||
# 3분 합성 시 1분만 있는 종목도 후보에 포함
|
||||
synth_on = (
|
||||
tail_tf == 3
|
||||
and get_env_bool("TAIL_BT_SYNTH_3M_FROM_1M", True)
|
||||
)
|
||||
if synth_on:
|
||||
try:
|
||||
codes_1m = db.conn.execute(
|
||||
"SELECT DISTINCT code FROM ws_candles WHERE timeframe=1 "
|
||||
"AND candle_time >= %s AND candle_time <= %s ORDER BY code",
|
||||
[start_key, end_key],
|
||||
).fetchall()
|
||||
for r in codes_1m:
|
||||
c = r["code"]
|
||||
if c not in codes:
|
||||
codes.append(c)
|
||||
codes = sorted(set(codes))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
candles_by_code: Dict[str, List[Dict]] = {}
|
||||
total_candles = 0
|
||||
min_bars = int(rsi_period) + 5
|
||||
synth_filled_total = 0
|
||||
|
||||
for code in codes:
|
||||
rows = db.conn.execute(
|
||||
@@ -116,18 +376,78 @@ def load_tail_candles_by_code(
|
||||
"ORDER BY candle_time ASC",
|
||||
[tail_tf, code, start_key, end_key],
|
||||
).fetchall()
|
||||
if len(rows) < min_bars:
|
||||
bars = [dict(r) for r in rows]
|
||||
|
||||
if synth_on:
|
||||
bars, n_fill = _synth_fill_3m_holes_from_1m(
|
||||
db, code, bars, start_key, end_key, peak_sel=peak_sel,
|
||||
)
|
||||
synth_filled_total += n_fill
|
||||
|
||||
# 기간 내 1봉 이상이면 일단 적재 — 전일 웜업 후 min_bars 재필터
|
||||
if len(bars) < 1:
|
||||
continue
|
||||
candles_by_code[code] = [dict(r) for r in rows]
|
||||
total_candles += len(rows)
|
||||
candles_by_code[code] = bars
|
||||
|
||||
# B) 전일 TF봉 DB prepend (실매 get_candles n=50 정합)
|
||||
warmup_n = prepend_tail_candle_warmup(
|
||||
db, candles_by_code, start_key,
|
||||
timeframe=tail_tf, peak_sel=peak_sel,
|
||||
)
|
||||
|
||||
# 웜업 후에도 RSI+여유 미달 종목 제거
|
||||
for code in list(candles_by_code.keys()):
|
||||
if len(candles_by_code[code]) < min_bars:
|
||||
del candles_by_code[code]
|
||||
total_candles = sum(len(v) for v in candles_by_code.values())
|
||||
|
||||
materialize_ws_candles_batch(db, candles_by_code, tail_tf)
|
||||
if synth_on and synth_filled_total:
|
||||
import logging
|
||||
logging.getLogger("kis_trader.backtest.tail").info(
|
||||
"🔧 [백테-합성] 1M→3M 구멍 보강 %d봉 (종목 %d)",
|
||||
synth_filled_total, len(candles_by_code),
|
||||
)
|
||||
if warmup_n:
|
||||
import logging
|
||||
logging.getLogger("kis_trader.backtest.tail").info(
|
||||
"🔧 [백테-웜업] 전일 %dM prepend %d봉 (종목 %d, 목표 N=%d)",
|
||||
tail_tf, warmup_n, len(candles_by_code), tail_backtest_candle_warmup_bars(),
|
||||
)
|
||||
return candles_by_code, total_candles, has_holding_peak
|
||||
|
||||
|
||||
def _synth_fill_3m_holes_from_1m(
|
||||
db,
|
||||
code: str,
|
||||
bars_3m: List[Dict],
|
||||
start_key: str,
|
||||
end_key: str,
|
||||
*,
|
||||
peak_sel: str = "",
|
||||
) -> Tuple[List[Dict], int]:
|
||||
"""DB 3분 리스트에 없는 시각만 1분→3분 롤업으로 보강."""
|
||||
from kis_trader.engine.candle_rollup import merge_fill_holes, rollup_1m_bars_to_tf
|
||||
|
||||
try:
|
||||
rows_1m = db.conn.execute(
|
||||
f"SELECT candle_time, open, high, low, close, volume{peak_sel} "
|
||||
"FROM ws_candles WHERE timeframe=1 AND code=%s "
|
||||
"AND candle_time >= %s AND candle_time <= %s AND is_confirmed=1 "
|
||||
"ORDER BY candle_time ASC",
|
||||
[code, start_key, end_key],
|
||||
).fetchall()
|
||||
except Exception:
|
||||
return bars_3m, 0
|
||||
if not rows_1m:
|
||||
return bars_3m, 0
|
||||
rolled = rollup_1m_bars_to_tf([dict(r) for r in rows_1m], 3)
|
||||
return merge_fill_holes(bars_3m, rolled)
|
||||
|
||||
|
||||
def _t2dt(candle_time: str) -> datetime:
|
||||
s = str(candle_time)
|
||||
return datetime.strptime(s[:12], "%Y%m%d%H%M")
|
||||
from kis_trader.utils.trade_time import parse_trade_datetime
|
||||
return parse_trade_datetime(candle_time)
|
||||
|
||||
|
||||
def attach_tail_trade_pnl(
|
||||
@@ -314,6 +634,9 @@ def run_tail_backtest_web_aligned(
|
||||
|
||||
engine_params = dict(params)
|
||||
engine_params["slot_money"] = float(slot_money)
|
||||
# 종목 일일 손익 게이트가 실매 realized_pnl과 동일 net 기준으로 온라인 누적하도록 전달
|
||||
engine_params["fee_rate"] = float(fee_rate)
|
||||
engine_params["sell_tax"] = float(sell_tax)
|
||||
if max_stocks is not None:
|
||||
engine_params["max_stocks"] = int(max_stocks)
|
||||
if total_budget_krw is not None:
|
||||
@@ -321,8 +644,40 @@ def run_tail_backtest_web_aligned(
|
||||
engine_params["total_budget_krw"] = tb if tb > 0 else float(
|
||||
int(engine_params.get("max_stocks") or 3) * slot_money
|
||||
)
|
||||
# 웜업봉은 지표용 — 매매 시계는 백테 기간일만 (전일 all_times 오염 방지)
|
||||
_sk0 = str((meta_out or {}).get("start_key") or "")[:12]
|
||||
if len(_sk0) >= 8:
|
||||
engine_params["_bt_period_start_ymd"] = _sk0[:8]
|
||||
|
||||
# B) DB 전일 부족 시 REST 1회 (유니버스 종목 우선, 모멘텀과 동일 패턴)
|
||||
_tf_rest = int(engine_params.get("timeframe") or engine_params.get("tf") or 3)
|
||||
if _tf_rest not in VALID_TIMEFRAMES:
|
||||
_tf_rest = 3
|
||||
if _sk0:
|
||||
rest_stats = inject_tail_rest_warmup_memory(
|
||||
candles_by_code,
|
||||
_sk0,
|
||||
timeframe=_tf_rest,
|
||||
universe_by_slot=universe_by_slot,
|
||||
)
|
||||
if meta_out is not None and (
|
||||
rest_stats.get("ok") or rest_stats.get("need") or rest_stats.get("skipped")
|
||||
):
|
||||
meta_out.setdefault("skip_stats", {})
|
||||
# skip_stats 는 엔진 후 overwrite 되므로 임시 보관
|
||||
meta_out["_tail_rest_warmup"] = dict(rest_stats)
|
||||
meta_out["_tail_candle_warmup_bars"] = tail_backtest_candle_warmup_bars()
|
||||
|
||||
if universe_by_slot is not None:
|
||||
engine_params.setdefault("scan_interval_min", 1)
|
||||
# 실매 정합 — skip_hts_scan_dupes 는 DB(TAIL_SKIP_HTS_SCAN_DUPES) 값을 그대로 쓴다.
|
||||
# (과거엔 저장 이력 백테 시 이 값을 True 로 강제했으나, 실매와 다른 값을 쓰게 되어
|
||||
# 정합성이 깨졌다. 이제는 params 에 이미 들어온 값(= 호출측이 DB/그리드에서 읽은 값)을
|
||||
# 그대로 사용 — 웹·파라서치·실매가 항상 같은 소스를 본다.)
|
||||
engine_params.setdefault("skip_hts_scan_dupes", te.resolve_tail_skip_hts_scan_dupes())
|
||||
if meta_out is not None:
|
||||
meta_out["skip_hts_scan_dupes_effective"] = bool(engine_params.get("skip_hts_scan_dupes"))
|
||||
meta_out["skip_hts_scan_dupes_requested"] = bool(params.get("skip_hts_scan_dupes"))
|
||||
engine_params.setdefault("portfolio_mode", True)
|
||||
|
||||
# ── 초단위 유니버스 타임라인 (실매 get_universe_at 정합, 돌파·모멘텀 공통) ──────
|
||||
@@ -341,17 +696,25 @@ def run_tail_backtest_web_aligned(
|
||||
if _days:
|
||||
_sk, _ek = min(_days), max(_days)
|
||||
if len(_sk) >= 8 and len(_ek) >= 8:
|
||||
from kis_trader.backtest.universe_timeline import build_universe_timeline
|
||||
from kis_trader.backtest.universe_timeline import (
|
||||
build_universe_timeline,
|
||||
resolve_universe_exit_debounce_sec,
|
||||
)
|
||||
_deb = resolve_universe_exit_debounce_sec(
|
||||
strategy_env_key="TAIL_UNIVERSE_EXIT_DEBOUNCE_SEC",
|
||||
default_when_no_grace=0,
|
||||
)
|
||||
_tl = build_universe_timeline(
|
||||
strategy_id=TAIL_STRATEGY_ID,
|
||||
start_ymd=_sk[:8], end_ymd=_ek[:8],
|
||||
debounce_sec=0, strict=False, strict_lag_minutes=0,
|
||||
debounce_sec=_deb, strict=False, strict_lag_minutes=0,
|
||||
)
|
||||
if _tl is not None:
|
||||
engine_params["_universe_timeline"] = _tl
|
||||
if meta_out is not None:
|
||||
meta_out["universe_timing"] = "scan_at"
|
||||
meta_out["universe_timeline_snapshots"] = _tl.snapshot_count
|
||||
meta_out["universe_exit_debounce_sec"] = _deb
|
||||
|
||||
# ── 체결 검증 게이트 (STRICT_FILL_VERIFY) ──────────────────────────────
|
||||
# 백테 체결모델(슬리피지·체결량 상한)은 '엄격 체결 검증'이 켜졌을 때만 적용한다.
|
||||
@@ -396,6 +759,8 @@ def run_tail_backtest_web_aligned(
|
||||
candles_by_code, engine_params, strategy="TAIL", meta_out=meta_out,
|
||||
orderbook_by_code=orderbook_by_code, program_by_code=program_by_code,
|
||||
)
|
||||
from kis_trader.backtest.backtest_env_timeline import attach_backtest_env_timeline_to_params
|
||||
attach_backtest_env_timeline_to_params(engine_params, meta_out, TAIL_STRATEGY_ID)
|
||||
if snap_meta.get("log_verdict_by_code"):
|
||||
engine_params["_backtest_log_verdict_by_code"] = snap_meta["log_verdict_by_code"]
|
||||
|
||||
@@ -427,8 +792,18 @@ def run_tail_backtest_web_aligned(
|
||||
if meta_out is not None:
|
||||
skip_stats = engine_params.get("_portfolio_skip_stats") or {}
|
||||
meta_out["skip_stats"] = dict(skip_stats)
|
||||
if meta_out.get("_tail_rest_warmup"):
|
||||
meta_out["skip_stats"]["rest_warmup"] = meta_out.pop("_tail_rest_warmup")
|
||||
if meta_out.get("_tail_candle_warmup_bars") is not None:
|
||||
meta_out["skip_stats"]["candle_warmup_bars"] = meta_out.pop(
|
||||
"_tail_candle_warmup_bars"
|
||||
)
|
||||
meta_out["engine_params"] = engine_params
|
||||
if tick_meta:
|
||||
from kis_trader.backtest.tail_tick_loader import enrich_tick_meta_with_traded_codes
|
||||
tick_meta = enrich_tick_meta_with_traded_codes(
|
||||
tick_meta, candles_by_code, loaded_ticks, trades,
|
||||
)
|
||||
meta_out["tick_backtest"] = tick_meta
|
||||
if int(tick_meta.get("ws_tick_rows_loaded") or 0) > 0:
|
||||
meta_out["backtest_buy_source"] = "ws_ticks"
|
||||
|
||||
Reference in New Issue
Block a user