fix: address critical code review issues (PnL double recording, sync HTTP, race conditions)

- fix(bot): prevent PnL double recording in _close_and_reenter using asyncio.Event
- fix(bot): prevent SYNC detection PnL duplication with _close_handled_by_sync flag
- fix(notifier): move sync HTTP call to background thread via run_in_executor
- fix(risk_manager): make is_trading_allowed async with lock for thread safety
- fix(exchange): cache exchange info at class level (1 API call for all symbols)
- fix(exchange): use `is not None` instead of truthy check for price/stop_price
- refactor(backtester): extract _calc_trade_stats to eliminate code duplication
- fix(ml_features): apply rolling z-score to OI/funding rate in serving (train-serve skew)
- fix(bot): use config.correlation_symbols instead of hardcoded BTCUSDT/ETHUSDT
- fix(bot): expand OI/funding history deque to 96 for z-score window
- cleanup(config): remove unused stop_loss_pct, take_profit_pct, trailing_stop_pct fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-19 23:03:52 +09:00
parent 24ed7ddec0
commit 181f82d3c0
9 changed files with 189 additions and 182 deletions

View File

@@ -7,6 +7,9 @@ from src.config import Config
class BinanceFuturesClient:
# 클래스 레벨 exchange info 캐시 (전체 심볼 1회만 조회)
_exchange_info_cache: dict | None = None
def __init__(self, config: Config, symbol: str = None):
self.config = config
self.symbol = symbol or config.symbol
@@ -19,10 +22,21 @@ class BinanceFuturesClient:
MIN_NOTIONAL = 5.0 # 바이낸스 선물 최소 명목금액 (USDT)
@classmethod
def _get_exchange_info(cls, client: Client) -> dict | None:
"""exchange info를 클래스 레벨로 캐시하여 1회만 조회한다."""
if cls._exchange_info_cache is None:
try:
cls._exchange_info_cache = client.futures_exchange_info()
except Exception as e:
logger.warning(f"exchange info 조회 실패: {e}")
return None
return cls._exchange_info_cache
def _load_symbol_precision(self) -> None:
"""바이낸스 exchange info에서 심볼별 수량/가격 정밀도를 로드한다."""
try:
info = self.client.futures_exchange_info()
info = self._get_exchange_info(self.client)
if info is not None:
for s in info["symbols"]:
if s["symbol"] == self.symbol:
self._qty_precision = s.get("quantityPrecision", 1)
@@ -32,12 +46,8 @@ class BinanceFuturesClient:
)
return
logger.warning(f"[{self.symbol}] exchange info에서 심볼 미발견, 기본 정밀도 사용")
self._qty_precision = 1
self._price_precision = 2
except Exception as e:
logger.warning(f"[{self.symbol}] exchange info 조회 실패 ({e}), 기본 정밀도 사용")
self._qty_precision = 1
self._price_precision = 2
self._qty_precision = 1
self._price_precision = 2
@property
def qty_precision(self) -> int:
@@ -109,10 +119,10 @@ class BinanceFuturesClient:
quantity=quantity,
reduceOnly=reduce_only,
)
if price:
if price is not None:
params["price"] = price
params["timeInForce"] = "GTC"
if stop_price:
if stop_price is not None:
params["stopPrice"] = stop_price
try:
return await loop.run_in_executor(