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:
@@ -6,14 +6,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import warnings
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import dataclass, field, asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import joblib
|
||||||
|
import lightgbm as lgb
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from loguru import logger
|
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:
|
def _calc_trade_stats(trades: list[dict], initial_balance: float) -> dict:
|
||||||
"""거래 리스트에서 통계 요약을 계산한다. Backtester와 WalkForward 공통 사용."""
|
"""거래 리스트에서 통계 요약을 계산한다. Backtester와 WalkForward 공통 사용."""
|
||||||
@@ -43,7 +49,7 @@ def _calc_trade_stats(trades: list[dict], initial_balance: float) -> dict:
|
|||||||
|
|
||||||
if len(pnls) > 1:
|
if len(pnls) > 1:
|
||||||
pnl_arr = np.array(pnls)
|
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:
|
else:
|
||||||
sharpe = 0.0
|
sharpe = 0.0
|
||||||
|
|
||||||
@@ -81,11 +87,6 @@ def _calc_trade_stats(trades: list[dict], initial_balance: float) -> dict:
|
|||||||
"close_reasons": reasons,
|
"close_reasons": reasons,
|
||||||
}
|
}
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import joblib
|
|
||||||
import lightgbm as lgb
|
|
||||||
|
|
||||||
from src.dataset_builder import (
|
from src.dataset_builder import (
|
||||||
_calc_indicators, _calc_signals, _calc_features_vectorized,
|
_calc_indicators, _calc_signals, _calc_features_vectorized,
|
||||||
generate_dataset_vectorized, stratified_undersample,
|
generate_dataset_vectorized, stratified_undersample,
|
||||||
|
|||||||
@@ -530,6 +530,7 @@ class TradingBot:
|
|||||||
self.current_trade_side = None
|
self.current_trade_side = None
|
||||||
self._entry_price = None
|
self._entry_price = None
|
||||||
self._entry_quantity = None
|
self._entry_quantity = None
|
||||||
|
self._close_event.set()
|
||||||
self._close_handled_by_sync = False
|
self._close_handled_by_sync = False
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -320,7 +320,16 @@ def build_features_aligned(
|
|||||||
else:
|
else:
|
||||||
base["oi_change_ma5"] = np.nan
|
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
|
base["adx"] = adx_z
|
||||||
|
|
||||||
return pd.Series(base)
|
return pd.Series(base)
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ class DiscordNotifier:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
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:
|
except RuntimeError:
|
||||||
self._send_sync(content)
|
self._send_sync(content)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user