feat: 자동매매 신뢰성 개선 — 시세 실서버 라우팅·일일 한도 전역화·수동 매수 API
- korea_investment_api: KIS_QUOTES_VIA_REAL — 모의투자 모드에서 시세/차트/호가 조회를 실계좌 시세 서버(20/s)로 위임, 주문·잔고는 모의서버 유지 (EGW00201 대응) - recommendations: execute_auto_trade_background asyncio.Lock 중복 실행 방지 + MAX_DAILY_TRADES를 당일 BUY 건수 차감 전역 기준으로 적용 (11종목 동시 매수 재발 방지) - manual_buy.py 신규: POST /api/v1/portfolio/buy (리스크 가드 + safe_place_order + db_worker) - mcp-server: claude_code_client(claude -p 헤드리스 모드) 추가, trade_monitor 부분매도 시 active 유지·remaining_pct 갱신, API 키 헤더·/api/v1 경로 수정, ODNO 주문ID 수정 - config: MCP_REQUEST_TIMEOUT, MCP_RISK_CACHE_TTL, BATCH_ANALYSIS_INTERVAL, SWING_INDEX_BREAKER_ENABLED(OFF), API_TRIGGERED_AUTO_TRADE(OFF) 추가 - docker-compose: 포트 충돌 해소 (postgres 15433, frontend 3001) - frontend: 추천 패널 매수 버튼, 차트/호가/저널 패널 개선 - CLAUDE.md 압축 최적화 (규칙 변경 없음), _sweep_single_axis.py 백테스트 단일축 스윕 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1220,6 +1220,11 @@ async def _execute_sell(
|
||||
await _release_order_lock(stock_code, "SELL")
|
||||
|
||||
|
||||
# execute_auto_trade_background 동시 실행 방지 — 스윙 스케줄러·API 트리거가 겹치면
|
||||
# 일일 거래 한도(MAX_DAILY_TRADES)가 호출 단위로만 적용돼 합산 초과됨 (2026-09-17 11종목 동시 매수 사고).
|
||||
_auto_trade_bg_lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def execute_auto_trade_background(recommendations_response: dict, db: AsyncSession):
|
||||
"""recommendations_response dict 에서 추천 종목을 꺼내 자동매매 실행.
|
||||
|
||||
@@ -1234,6 +1239,14 @@ async def execute_auto_trade_background(recommendations_response: dict, db: Asyn
|
||||
logger.error("[AUTO_TRADE_BG] recommendations_response is not a dict: %r", recommendations_response)
|
||||
return
|
||||
|
||||
if _auto_trade_bg_lock.locked():
|
||||
logger.info("[AUTO_TRADE_BG] 이전 실행이 진행 중 — 이번 호출 스킵 (중복 실행 방지)")
|
||||
return
|
||||
async with _auto_trade_bg_lock:
|
||||
await _execute_auto_trade_background_locked(recommendations_response, db)
|
||||
|
||||
|
||||
async def _execute_auto_trade_background_locked(recommendations_response: dict, db: AsyncSession):
|
||||
try:
|
||||
total_processed = 0
|
||||
trade_results = []
|
||||
@@ -1245,7 +1258,31 @@ async def execute_auto_trade_background(recommendations_response: dict, db: Asyn
|
||||
|
||||
regime_max_trades = market_regime.get("max_trades", 20) if market_regime else 20
|
||||
position_multiplier = market_regime.get("position_multiplier", 1.0) if market_regime else 1.0
|
||||
max_trades = min(20, regime_max_trades)
|
||||
max_trades = min(settings.MAX_DAILY_TRADES, regime_max_trades)
|
||||
|
||||
# 일일 한도는 "오늘 이미 나간 BUY 종목 수"를 차감한 전역 기준으로 적용
|
||||
# (호출 단위 슬라이스만으로는 여러 사이클에 걸쳐 한도 초과됨)
|
||||
try:
|
||||
_today_buys = await db.execute(
|
||||
select(TradeHistory.stock_code)
|
||||
.where(
|
||||
TradeHistory.order_type == "BUY",
|
||||
TradeHistory.status.in_(["COMPLETED", "PENDING"]),
|
||||
TradeHistory.created_at >= get_daily_cutoff_utc(),
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
_bought_today = len(_today_buys.scalars().all())
|
||||
except Exception as _e:
|
||||
logger.warning(f"[AUTO_TRADE_BG] 당일 매수 건수 조회 실패 (한도 미차감): {_e}")
|
||||
_bought_today = 0
|
||||
max_trades = max(0, max_trades - _bought_today)
|
||||
if max_trades == 0:
|
||||
logger.info(
|
||||
f"[AUTO_TRADE_BG] 일일 매수 한도 도달 (오늘 {_bought_today}종목 / 한도 "
|
||||
f"{min(settings.MAX_DAILY_TRADES, regime_max_trades)}) — 신규 진입 스킵"
|
||||
)
|
||||
return
|
||||
|
||||
result = await db.execute(select(Portfolio).where(Portfolio.quantity > 0))
|
||||
held_stocks = {p.stock_code for p in result.scalars().all()} # 현재 보유 종목 집합
|
||||
|
||||
Reference in New Issue
Block a user