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)