fix: address follow-up review findings

- 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) <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-19 23:10:02 +09:00
parent 181f82d3c0
commit e3a78974b3
4 changed files with 22 additions and 8 deletions

View File

@@ -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,

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)