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:
36
src/bot.py
36
src/bot.py
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user