fix: critical production issues — WebSocket reconnect, ghost positions, ONNX NaN

- fix(data_stream): add reconnect loop to MultiSymbolStream matching UserDataStream pattern
  Prevents bot-wide crash on WebSocket disconnect (#3 Critical)

- fix(data_stream): increase buffer_size 200→300 and preload 200→300
  Ensures z-score window (288) has sufficient data (#5 Important)

- fix(bot): sync risk manager when Binance has no position but local state does
  Prevents ghost entries in open_positions blocking future trades (#1 Critical)

- fix(ml_filter): add np.nan_to_num for ONNX input to handle NaN features
  Prevents all signals being blocked during initial ~2h warmup (#2 Critical)

- fix(bot): replace _close_handled_by_sync with current_trade_side==None guard
  Eliminates race window in SYNC PnL double recording (#4 Important)

- feat(bot): add _ensure_sl_tp_orders in _recover_position
  Detects and re-places missing SL/TP orders on bot restart (#6 Important)

- feat(exchange): add get_open_orders method for SL/TP verification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-19 23:37:47 +09:00
parent e3a78974b3
commit e648ae7ca0
4 changed files with 109 additions and 16 deletions

View File

@@ -10,8 +10,10 @@ from loguru import logger
_MIN_CANDLES_FOR_SIGNAL = 100
# 초기 구동 시 REST API로 가져올 과거 캔들 수.
# 15분봉 200개 = 50시간치 — EMA50(12.5h) 대비 4배 여유.
_PRELOAD_LIMIT = 200
# z-score 윈도우(288) + EMA50(50) 안정화 여유분. 15분봉 300개 = 75시간.
_PRELOAD_LIMIT = 300
_RECONNECT_DELAY = 5 # WebSocket 재연결 대기 초
@@ -105,7 +107,7 @@ class MultiSymbolStream:
self,
symbols: list[str],
interval: str = "15m",
buffer_size: int = 200,
buffer_size: int = 300,
on_candle: Callable = None,
):
self.symbols = [s.lower() for s in symbols]
@@ -199,9 +201,34 @@ class MultiSymbolStream:
]
logger.info(f"Combined WebSocket 시작: {streams}")
try:
async with bm.futures_multiplex_socket(streams) as stream:
while True:
msg = await stream.recv()
await self.handle_message(msg)
await self._run_loop(bm, streams)
finally:
await client.close_connection()
async def _run_loop(self, bm: BinanceSocketManager, streams: list[str]) -> None:
"""WebSocket 연결 → 재연결 무한 루프."""
while True:
try:
async with bm.futures_multiplex_socket(streams) as stream:
logger.info("Kline WebSocket 연결 완료")
while True:
msg = await stream.recv()
if isinstance(msg, dict) and msg.get("e") == "error":
logger.warning(
f"Kline WebSocket 에러 수신: {msg.get('m', msg)} — 재연결"
)
break
await self.handle_message(msg)
except asyncio.CancelledError:
logger.info("Kline WebSocket 정상 종료")
raise
except Exception as e:
logger.warning(
f"Kline WebSocket 끊김: {e}"
f"{_RECONNECT_DELAY}초 후 재연결"
)
await asyncio.sleep(_RECONNECT_DELAY)