Critical: - #2: Add _entry_lock in RiskManager to serialize concurrent entry (balance race) - #3: Add startTime to get_recent_income + record _entry_time_ms (SYNC PnL fix) Important: - #1: Add threading.Lock + _run_api() helper for thread-safe Client access - #4: Convert reset_daily to async with lock - #8: Add 24h TTL to exchange_info_cache Minor: - #7: Remove duplicate Indicators creation in _open_position (use ATR directly) - #11: Add input validation for LEVERAGE, MARGIN ratios, ML_THRESHOLD - #12: Replace hardcoded corr[0]/corr[1] with dict-based dynamic access - #14: Add fillna(0.0) to LightGBM path for NaN consistency with ONNX Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.2 KiB
Python
94 lines
3.2 KiB
Python
import asyncio
|
|
import signal
|
|
from datetime import datetime, timedelta, timezone
|
|
from dotenv import load_dotenv
|
|
from loguru import logger
|
|
from src.config import Config
|
|
from src.bot import TradingBot
|
|
from src.risk_manager import RiskManager
|
|
from src.logger_setup import setup_logger
|
|
|
|
load_dotenv()
|
|
|
|
|
|
async def _daily_reset_loop(risk: RiskManager):
|
|
"""매일 UTC 자정에 daily_pnl을 초기화한다."""
|
|
while True:
|
|
now = datetime.now(timezone.utc)
|
|
next_midnight = (now + timedelta(days=1)).replace(
|
|
hour=0, minute=0, second=0, microsecond=0,
|
|
)
|
|
await asyncio.sleep((next_midnight - now).total_seconds())
|
|
await 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()
|
|
risk = RiskManager(config)
|
|
|
|
# 기준 잔고를 main에서 한 번만 설정 (경쟁 조건 방지)
|
|
from src.exchange import BinanceFuturesClient
|
|
exchange = BinanceFuturesClient(config, symbol=config.symbols[0])
|
|
balance = await exchange.get_balance()
|
|
risk.set_base_balance(balance)
|
|
logger.info(f"기준 잔고 설정: {balance:.2f} USDT")
|
|
|
|
bots = []
|
|
for symbol in config.symbols:
|
|
bot = TradingBot(config, symbol=symbol, risk=risk)
|
|
bots.append(bot)
|
|
|
|
logger.info(f"멀티심볼 봇 시작: {config.symbols} ({len(bots)}개 인스턴스)")
|
|
|
|
# 시그널 핸들러 등록
|
|
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())
|