From e3a78974b32d00e5e77b6a6e56c4ebd2c292e9a5 Mon Sep 17 00:00:00 2001 From: 21in7 Date: Thu, 19 Mar 2026 23:10:02 +0900 Subject: [PATCH] fix: address follow-up review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(notifier): capture fire-and-forget Future exceptions via done_callback - fix(bot): add _close_event.set() in SYNC path to unblock _close_and_reenter - fix(ml_features): apply z-score to oi_price_spread (oi_z - ret_1_z) matching training - fix(backtester): clean up import ordering after _calc_trade_stats extraction - fix(backtester): correct Sharpe annualization for 24/7 crypto (365d × 96 = 35,040) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/backtester.py | 13 +++++++------ src/bot.py | 1 + src/ml_features.py | 11 ++++++++++- src/notifier.py | 5 ++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/backtester.py b/src/backtester.py index 6860d75..a7e30ca 100644 --- a/src/backtester.py +++ b/src/backtester.py @@ -6,14 +6,20 @@ from __future__ import annotations import json +import warnings from dataclasses import dataclass, field, asdict from datetime import datetime from pathlib import Path +import joblib +import lightgbm as lgb import numpy as np import pandas as pd from loguru import logger +# 크립토 24/7 시장: 15분봉 × 96봉/일 × 365일 = 35,040 +_ANNUALIZE_FACTOR = 35_040 + def _calc_trade_stats(trades: list[dict], initial_balance: float) -> dict: """거래 리스트에서 통계 요약을 계산한다. Backtester와 WalkForward 공통 사용.""" @@ -43,7 +49,7 @@ def _calc_trade_stats(trades: list[dict], initial_balance: float) -> dict: if len(pnls) > 1: pnl_arr = np.array(pnls) - sharpe = float(np.mean(pnl_arr) / np.std(pnl_arr) * np.sqrt(24192)) if np.std(pnl_arr) > 0 else 0.0 + sharpe = float(np.mean(pnl_arr) / np.std(pnl_arr) * np.sqrt(_ANNUALIZE_FACTOR)) if np.std(pnl_arr) > 0 else 0.0 else: sharpe = 0.0 @@ -81,11 +87,6 @@ def _calc_trade_stats(trades: list[dict], initial_balance: float) -> dict: "close_reasons": reasons, } -import warnings - -import joblib -import lightgbm as lgb - from src.dataset_builder import ( _calc_indicators, _calc_signals, _calc_features_vectorized, generate_dataset_vectorized, stratified_undersample, diff --git a/src/bot.py b/src/bot.py index e0668b0..bb7b020 100644 --- a/src/bot.py +++ b/src/bot.py @@ -530,6 +530,7 @@ class TradingBot: self.current_trade_side = None self._entry_price = None self._entry_quantity = None + self._close_event.set() self._close_handled_by_sync = False continue except Exception as e: diff --git a/src/ml_features.py b/src/ml_features.py index 4fbeae0..fde2f36 100644 --- a/src/ml_features.py +++ b/src/ml_features.py @@ -320,7 +320,16 @@ def build_features_aligned( else: base["oi_change_ma5"] = np.nan - base["oi_price_spread"] = float(oi_price_spread) if oi_price_spread is not None else np.nan + # oi_price_spread = oi_z - ret_1_z (학습과 동일하게 z-score 적용된 값의 차이) + if oi_history and len(oi_history) >= 2 and oi_price_spread is not None: + oi_z = base.get("oi_change", np.nan) + ret_1_z = base.get("ret_1", 0.0) + if not np.isnan(oi_z): + base["oi_price_spread"] = oi_z - ret_1_z + else: + base["oi_price_spread"] = np.nan + else: + base["oi_price_spread"] = np.nan base["adx"] = adx_z return pd.Series(base) diff --git a/src/notifier.py b/src/notifier.py index 7a147c3..ce39ab5 100644 --- a/src/notifier.py +++ b/src/notifier.py @@ -17,7 +17,10 @@ class DiscordNotifier: return try: loop = asyncio.get_running_loop() - loop.run_in_executor(None, self._send_sync, content) + fut = loop.run_in_executor(None, self._send_sync, content) + fut.add_done_callback( + lambda f: f.exception() and logger.warning(f"Discord 전송 실패: {f.exception()}") + ) except RuntimeError: self._send_sync(content)