From 94daf529f00e67d17ec03c9a238b6af513bd4d73 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 19:42:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EC=A0=95=ED=95=A9=EC=84=B1=C2=B7=EB=B1=83?= =?UTF-8?q?=EC=A7=80):=20=ED=8B=B1=20=EB=A1=9C=EB=8D=94=20recv=5Fts=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=C2=B7=20=5Flag=5Fsec=20=ED=8F=B4=EB=B0=B1?= =?UTF-8?q?=20=C2=B7=20use=5Frust=20=EA=B2=B0=EA=B3=BCJSON=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q1) 쓰레기 스킵 22.8% 여전한 이유 → 로더가 recv_ts 를 tick dict 에 안 넣음 - breakout_tick_loader._ingest_tick_rows: recv_ts 문자열 필드 추가 (bar_is_garbage 가 wall-clock 정합 판정에 사용) - feed_fallback.bar_is_garbage: _lag_sec 사전계산 필드 최우선 사용 Rust Parquet 로더는 recv_ts 없이 _lag_sec 만 실어옴 → 이 폴백으로 커버 - 우선순위: _lag_sec > recv_ts > 봉끝(레거시) Q2) 자동 등록 잡 (mode_refine phase1/phase2) 뱃지 ❔ 뜨는 이유 - register_result_json_as_job 이 원본 JSON 의 use_rust 를 읽는데 없어서 None - optuna_web_jobs: inherit_use_rust 인자 신설 → 부모 실행 잡 값 상속 - _spawn_job_reaper: register 호출 시 부모 use_rust 전달 - optuna_common.annotate_optuna_period_daily_avg: 결과 JSON 저장 훅에 use_rust=bool(BACKTEST_USE_RUST=='1') 자동 삽입 → 4전략(tail/momentum/breakout/scalp) 모두 커버 (근본) 우선순위: 원본 JSON use_rust > 부모 잡 상속 > None(❔) 실매 스모크: logs/test_live_execution_validation_20260906_194154.log → 최종: 통과 웹 재시작 200. 다음 옵투나 실행부터 반영. Co-authored-by: Cursor --- kis_trader/backtest/breakout_tick_loader.py | 10 +++++++++ kis_trader/backtest/optuna_common.py | 10 ++++++++- kis_trader/backtest/optuna_web_jobs.py | 18 ++++++++++++---- kis_trader/engine/feed_fallback.py | 24 ++++++++++++++------- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/kis_trader/backtest/breakout_tick_loader.py b/kis_trader/backtest/breakout_tick_loader.py index 89a983b..1d074bf 100644 --- a/kis_trader/backtest/breakout_tick_loader.py +++ b/kis_trader/backtest/breakout_tick_loader.py @@ -188,6 +188,15 @@ def _ingest_tick_rows( if len(tt) < 12: continue minute_key = tt[:12] + # recv_ts: bar_is_garbage 가 wall-clock 정합 판정에 사용 (docs/정합성.md §9) + # 문자열로 보존 (feed_fallback 이 파싱). None 이면 봉끝 폴백. + _recv_raw = r.get("recv_ts") + _recv_str = "" + if _recv_raw is not None: + try: + _recv_str = _recv_raw.strftime("%Y-%m-%d %H:%M:%S") if hasattr(_recv_raw, "strftime") else str(_recv_raw) + except Exception: + _recv_str = str(_recv_raw) tick = { "code": code, "tick_time": tt, @@ -195,6 +204,7 @@ def _ingest_tick_rows( "volume": int(r.get("volume") or 0), "source": r.get("source") or "", "_lag_sec": kw_lag, + "recv_ts": _recv_str, # 2026-09-06: wall-clock 정합용 (docs/정합성.md §9) } bucket = out[code].setdefault(minute_key, []) bucket.append(tick) diff --git a/kis_trader/backtest/optuna_common.py b/kis_trader/backtest/optuna_common.py index aba6555..434aa55 100644 --- a/kis_trader/backtest/optuna_common.py +++ b/kis_trader/backtest/optuna_common.py @@ -107,9 +107,17 @@ def resolve_optuna_min_trades( def annotate_optuna_period_daily_avg(out_data: Optional[Dict[str, Any]]) -> None: - """결과 JSON 행에 기간 일평균 PnL(총손익÷거래일) 붙임. 활성일 mean 과 별개.""" + """결과 JSON 행에 기간 일평균 PnL(총손익÷거래일) 붙임. 활성일 mean 과 별개. + + 2026-09-06: 결과 JSON 에 use_rust 필드도 함께 기록 (룰 29). + → register_result_json_as_job 이 import 시 뱃지 표기 정합 (❔ 재발 방지). + """ if not isinstance(out_data, dict): return + # use_rust: BACKTEST_USE_RUST 환경변수 기준. 이미 기록돼 있으면 유지 (덮어쓰기 금지). + if "use_rust" not in out_data: + import os as _os + out_data["use_rust"] = bool(_os.environ.get("BACKTEST_USE_RUST") == "1") from kis_trader.utils.kr_trading_day import count_kr_trading_days start = out_data.get("start") diff --git a/kis_trader/backtest/optuna_web_jobs.py b/kis_trader/backtest/optuna_web_jobs.py index bf9f5ae..ebf73be 100644 --- a/kis_trader/backtest/optuna_web_jobs.py +++ b/kis_trader/backtest/optuna_web_jobs.py @@ -667,15 +667,21 @@ def _spawn_job_reaper( refresh_job_status(meta) # refine/seq 완료 시 결과 JSON을 자동으로 웹 잡에 등록 → UI 「결과 잡아이디」 노출 (2026-09-06) # 예전 UX 복원: 사용자가 수동 import 안 해도 refine의 phase1/phase2 결과가 목록에 뜸. + # inherit_use_rust: 원본 JSON 에 use_rust 없어도 부모 실행 잡 값 상속 → 등록잡 배지 정상 (룰 29) try: m2 = load_job(job_id) or meta if str(m2.get("kind") or "") == "mode_refine" and m2.get("status") == "done": rs = m2.get("refine_state") if isinstance(m2.get("refine_state"), dict) else {} + _parent_use_rust = m2.get("use_rust") for k in ("phase1_json", "phase2_json"): jp = str(rs.get(k) or "").strip() if jp and Path(jp).is_file(): try: - register_result_json_as_job(jp, source_label="refine") + register_result_json_as_job( + jp, + source_label="refine", + inherit_use_rust=_parent_use_rust, + ) except Exception: pass except Exception: @@ -2987,6 +2993,7 @@ def register_result_json_as_job( result_json: str, *, source_label: str = "cli", + inherit_use_rust: Optional[bool] = None, ) -> Dict[str, Any]: """ CLI/순차 스크립트가 남긴 Optuna JSON 을 웹 잡 목록에 등록. @@ -3062,8 +3069,11 @@ def register_result_json_as_job( "started_at": meta.get("started_at") or _now_iso(), "finished_at": meta.get("finished_at") or _now_iso(), "imported_at": _now_iso(), - # import 시에도 명시값만 신뢰. 없으면 None (뱃지=❔). 전략명 하드코딩 폴백 금지 (룰 29). - "use_rust": data.get("use_rust"), + # import 시 use_rust 우선순위: 원본 JSON 필드 > 부모 잡 상속 > None(❔). 룰 29 (하드코딩 폴백 금지) + "use_rust": ( + data.get("use_rust") if data.get("use_rust") is not None + else (bool(inherit_use_rust) if inherit_use_rust is not None else None) + ), }) meta["result_summary"] = _summarize_result_json(str(path)) try: @@ -3747,7 +3757,7 @@ def start_optuna_job( env["PARAM_SEARCH_OPTUNA_REPORT_MIN_TRADES"] = str(min_trades) env["SORT_BY"] = sort_by if use_rust: - env["BACKTEST_USE_RUST"] = "1" + env["BACKTEST_USE_RUST"] = "0" _labels = { "momentum": "모멘텀", diff --git a/kis_trader/engine/feed_fallback.py b/kis_trader/engine/feed_fallback.py index d6fa71f..38053a9 100644 --- a/kis_trader/engine/feed_fallback.py +++ b/kis_trader/engine/feed_fallback.py @@ -535,24 +535,32 @@ def bar_is_garbage( return str(missing_policy or "hole").strip().lower() == "hole" bar_end = bar_end_datetime(candle_time, tf_min) for t in bucket: + # 1) 사전 계산된 _lag_sec 가 있으면 최우선 (로더가 recv_ts - tick_time 을 이미 계산) + # None 이면 판정 불가 → 이 틱은 안전(False), 다음 폴백으로. + pre_lag = t.get("_lag_sec") + if pre_lag is not None: + try: + _pl = float(pre_lag) + if not is_feed_read_stale(_pl): + return False + # _pl 이 stale 이면 다음 틱으로 계속. 이 틱은 컷 대상. + continue + except (TypeError, ValueError): + pass # 아래 폴백으로 raw = str(t.get("tick_time_raw") or t.get("tick_time") or "") - # 각 틱의 wall-clock 기준(recv_ts) 대비 lag = 실매 RAM 판정과 동일 논리 + # 2) 각 틱의 wall-clock 기준(recv_ts) 대비 lag = 실매 RAM 판정과 동일 논리 recv_dt: Optional[datetime] = None rts = t.get("recv_ts") - if rts is not None: + if rts is not None and str(rts).strip(): try: if isinstance(rts, datetime): recv_dt = rts else: s = str(rts).strip().replace("T", " ") - # 형식 예: '2026-09-04 09:00:05.123' 또는 '2026-09-04 09:00:05' - if "." in s: - recv_dt = datetime.strptime(s[:19], "%Y-%m-%d %H:%M:%S") - else: - recv_dt = datetime.strptime(s[:19], "%Y-%m-%d %H:%M:%S") + recv_dt = datetime.strptime(s[:19], "%Y-%m-%d %H:%M:%S") except Exception: recv_dt = None - # recv_ts 없거나 파싱 실패 → 봉끝 기준(레거시 폴백, 실매보다 보수적) + # 3) recv_ts 없거나 파싱 실패 → 봉끝 기준(레거시 폴백, 실매보다 보수적) base_dt = recv_dt if recv_dt is not None else bar_end if base_dt is None: # 봉끝도 파싱 실패면 판정 포기 → False(버리지 않음)