fix: critical bugs — double fee, SL/TP atomicity, PnL race, graceful shutdown
C5: Remove duplicate entry_fee deduction in backtester (balance and net_pnl) C1: Add SL/TP retry (3x) with emergency market close on final failure C3: Add _close_lock to prevent PnL double recording between callback and monitor C8: Add SIGTERM/SIGINT handler with per-symbol order cancellation before exit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
main.py
50
main.py
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import signal
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from dotenv import load_dotenv
|
||||
from loguru import logger
|
||||
@@ -21,6 +22,25 @@ async def _daily_reset_loop(risk: RiskManager):
|
||||
risk.reset_daily()
|
||||
|
||||
|
||||
async def _graceful_shutdown(bots: list[TradingBot], tasks: list[asyncio.Task]):
|
||||
"""모든 봇의 오픈 주문 취소 후 태스크를 정리한다."""
|
||||
logger.info("Graceful shutdown 시작 — 오픈 주문 취소 중...")
|
||||
for bot in bots:
|
||||
try:
|
||||
await asyncio.wait_for(bot.exchange.cancel_all_orders(), timeout=5)
|
||||
logger.info(f"[{bot.symbol}] 오픈 주문 취소 완료")
|
||||
except Exception as e:
|
||||
logger.warning(f"[{bot.symbol}] 오픈 주문 취소 실패 (무시): {e}")
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
for r in results:
|
||||
if isinstance(r, Exception) and not isinstance(r, asyncio.CancelledError):
|
||||
logger.warning(f"태스크 종료 중 예외: {r}")
|
||||
logger.info("Graceful shutdown 완료")
|
||||
|
||||
|
||||
async def main():
|
||||
setup_logger(log_level="INFO")
|
||||
config = Config()
|
||||
@@ -39,11 +59,35 @@ async def main():
|
||||
bots.append(bot)
|
||||
|
||||
logger.info(f"멀티심볼 봇 시작: {config.symbols} ({len(bots)}개 인스턴스)")
|
||||
await asyncio.gather(
|
||||
*[bot.run() for bot in bots],
|
||||
_daily_reset_loop(risk),
|
||||
|
||||
# 시그널 핸들러 등록
|
||||
loop = asyncio.get_running_loop()
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
def _signal_handler():
|
||||
logger.warning("종료 시그널 수신 (SIGTERM/SIGINT)")
|
||||
shutdown_event.set()
|
||||
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
loop.add_signal_handler(sig, _signal_handler)
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(bot.run(), name=f"bot-{bot.symbol}")
|
||||
for bot in bots
|
||||
]
|
||||
tasks.append(asyncio.create_task(_daily_reset_loop(risk), name="daily-reset"))
|
||||
|
||||
# 종료 시그널 대기 vs 태스크 완료 (먼저 발생하는 쪽)
|
||||
shutdown_task = asyncio.create_task(shutdown_event.wait(), name="shutdown-wait")
|
||||
done, pending = await asyncio.wait(
|
||||
tasks + [shutdown_task],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
|
||||
# 시그널이든 태스크 종료든 graceful shutdown 수행
|
||||
shutdown_task.cancel()
|
||||
await _graceful_shutdown(bots, tasks)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user