fix: recreate AsyncClient on User Data Stream reconnect
After keepalive ping timeout, the existing BinanceSocketManager's listenKey becomes invalid. Reusing it causes silent failure — the WebSocket appears connected but receives no ORDER_TRADE_UPDATE events. This caused SL/TP fills to be detected only by the 5-minute position monitor polling (SYNC) instead of real-time User Data Stream (TP/SL), affecting 11 of 12 production trades. Fix: create fresh AsyncClient + BinanceSocketManager on every reconnect iteration, ensuring a valid listenKey is obtained each time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ class UserDataStream:
|
||||
"""
|
||||
Binance Futures User Data Stream을 구독하여 주문 체결 이벤트를 처리한다.
|
||||
|
||||
- python-binance BinanceSocketManager의 내장 keepalive 활용
|
||||
- 매 재연결마다 AsyncClient + BinanceSocketManager를 새로 생성 (listenKey 무효화 대응)
|
||||
- 네트워크 단절 시 무한 재연결 루프
|
||||
- ORDER_TRADE_UPDATE 이벤트에서 지정 심볼의 청산 주문만 필터링하여 콜백 호출
|
||||
- 부분 체결(PARTIALLY_FILLED) 시 rp/commission을 누적하여 최종 FILLED에서 합산 콜백
|
||||
@@ -30,20 +30,23 @@ class UserDataStream:
|
||||
|
||||
async def start(self, api_key: str, api_secret: str) -> None:
|
||||
"""User Data Stream 메인 루프 — 봇 종료 시까지 실행."""
|
||||
client = await AsyncClient.create(
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
)
|
||||
bm = BinanceSocketManager(client)
|
||||
try:
|
||||
await self._run_loop(bm)
|
||||
finally:
|
||||
await client.close_connection()
|
||||
await self._run_loop(api_key, api_secret)
|
||||
|
||||
async def _run_loop(self, bm: BinanceSocketManager) -> None:
|
||||
"""연결 → 재연결 무한 루프. BinanceSocketManager가 listenKey keepalive를 내부 처리한다."""
|
||||
async def _run_loop(self, api_key: str, api_secret: str) -> None:
|
||||
"""연결 → 재연결 무한 루프.
|
||||
|
||||
매 재연결마다 AsyncClient + BinanceSocketManager를 새로 생성한다.
|
||||
keepalive ping timeout 후 기존 BinanceSocketManager의 listenKey가
|
||||
무효화되면 재사용 시 이벤트를 수신하지 못하는 "조용한 실패"가 발생하므로,
|
||||
반드시 새 인스턴스를 만들어야 한다.
|
||||
"""
|
||||
while True:
|
||||
client = await AsyncClient.create(
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
)
|
||||
try:
|
||||
bm = BinanceSocketManager(client)
|
||||
async with bm.futures_user_socket() as stream:
|
||||
logger.info(f"User Data Stream 연결 완료 (심볼 필터: {self._symbol})")
|
||||
while True:
|
||||
@@ -60,6 +63,10 @@ class UserDataStream:
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("User Data Stream 정상 종료")
|
||||
try:
|
||||
await client.close_connection()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
@@ -67,7 +74,13 @@ class UserDataStream:
|
||||
f"User Data Stream 끊김: {e} — "
|
||||
f"{_RECONNECT_DELAY}초 후 재연결"
|
||||
)
|
||||
await asyncio.sleep(_RECONNECT_DELAY)
|
||||
finally:
|
||||
try:
|
||||
await client.close_connection()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await asyncio.sleep(_RECONNECT_DELAY)
|
||||
|
||||
async def _handle_message(self, msg: dict) -> None:
|
||||
"""ORDER_TRADE_UPDATE 이벤트에서 청산 주문을 필터링하여 콜백을 호출한다."""
|
||||
|
||||
Reference in New Issue
Block a user