fix: fetch actual PnL from Binance income API on SYNC close detection

When the position monitor detects a missed close via API fallback, it
now queries Binance futures_income_history to get the real realized PnL
and commission instead of logging zeros. Exit price is estimated from
entry price + PnL/quantity. This ensures the dashboard records accurate
profit data even when WebSocket events are missed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-14 12:43:27 +09:00
parent 363234ac7c
commit 805f1b0528
2 changed files with 49 additions and 8 deletions

View File

@@ -320,20 +320,40 @@ class TradingBot:
f"[{self.symbol}] 포지션 불일치 감지: " f"[{self.symbol}] 포지션 불일치 감지: "
f"봇={self.current_trade_side}, 바이낸스=포지션 없음 — 상태 동기화" f"봇={self.current_trade_side}, 바이낸스=포지션 없음 — 상태 동기화"
) )
estimated_pnl = 0.0 # Binance income API에서 실제 PnL 조회
await self.risk.close_position(self.symbol, estimated_pnl) realized_pnl = 0.0
commission = 0.0
exit_price = 0.0
try:
pnl_rows, comm_rows = await self.exchange.get_recent_income(limit=5)
if pnl_rows:
realized_pnl = float(pnl_rows[-1].get("income", "0"))
if comm_rows:
commission = abs(float(comm_rows[-1].get("income", "0")))
except Exception:
pass
net_pnl = realized_pnl - commission
# exit_price 추정: 진입가 + PnL/수량
if self._entry_quantity and self._entry_quantity > 0 and self._entry_price:
if self.current_trade_side == "LONG":
exit_price = self._entry_price + realized_pnl / self._entry_quantity
else:
exit_price = self._entry_price - realized_pnl / self._entry_quantity
await self.risk.close_position(self.symbol, net_pnl)
self.notifier.notify_close( self.notifier.notify_close(
symbol=self.symbol, symbol=self.symbol,
side=self.current_trade_side, side=self.current_trade_side,
close_reason="SYNC", close_reason="SYNC",
exit_price=0.0, exit_price=exit_price,
estimated_pnl=0.0, estimated_pnl=realized_pnl,
net_pnl=0.0, net_pnl=net_pnl,
diff=0.0, diff=net_pnl - realized_pnl,
) )
logger.info( logger.info(
f"[{self.symbol}] 청산 감지(SYNC): exit=0.0000, " f"[{self.symbol}] 청산 감지(SYNC): exit={exit_price:.4f}, "
f"rp=+0.0000, commission=0.0000, net_pnl=+0.0000" f"rp={realized_pnl:+.4f}, commission={commission:.4f}, "
f"net_pnl={net_pnl:+.4f}"
) )
self.current_trade_side = None self.current_trade_side = None
self._entry_price = None self._entry_price = None

View File

@@ -193,6 +193,27 @@ class BinanceFuturesClient:
except Exception as e: except Exception as e:
logger.warning(f"Algo 주문 전체 취소 실패 (무시): {e}") logger.warning(f"Algo 주문 전체 취소 실패 (무시): {e}")
async def get_recent_income(self, limit: int = 5) -> list[dict]:
"""최근 REALIZED_PNL + COMMISSION 내역을 조회한다."""
loop = asyncio.get_event_loop()
try:
rows = await loop.run_in_executor(
None,
lambda: self.client.futures_income_history(
symbol=self.symbol, incomeType="REALIZED_PNL", limit=limit,
),
)
commissions = await loop.run_in_executor(
None,
lambda: self.client.futures_income_history(
symbol=self.symbol, incomeType="COMMISSION", limit=limit,
),
)
return rows, commissions
except Exception as e:
logger.warning(f"[{self.symbol}] 수익 내역 조회 실패: {e}")
return [], []
async def get_open_interest(self) -> float | None: async def get_open_interest(self) -> float | None:
"""현재 미결제약정(OI)을 조회한다. 오류 시 None 반환.""" """현재 미결제약정(OI)을 조회한다. 오류 시 None 반환."""
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()