fix: 자동매매 신뢰성·소액계좌 안전배치 5종
감사에서 드러난 신뢰성·조용한실패 버그 수정. 전략값(§5)·시그니처(§4) 불변. ① KIS API 신뢰성(korea_investment_api, config): 스로틀에 _throttle_lock 추가로 thundering herd(8코루틴 동시발사) 제거, API_TIMEOUT 10→20초, circuit_breaker 임계 15→7, 재시도 백오프 지터. → 시간당 121건 실패의 근원 완화. ② portfolio summary 보존(portfolio): 잔고 API 실패 시 summary가 영영 None으로 남아 현금 0→매수 스킵되던 것을, 최초 실패 시 holdings 기반 부분 summary로 채움. ③ output2 빈리스트 가드: KIS가 output2=[] 반환 시 IndexError로 MDD 업데이트가 조용히 스킵(하방보호 비활성)되던 것 방어. swing/scheduler·adaptive_strategy (실버그) + daytrading 2곳(정규화). trade_executor:555는 load-bearing 가드라 의도적 미변경. ④ MIN_BUY_AMOUNT config화+소액완화(trade_executor, config): 하드코딩 50만을 MIN_BUY_AMOUNT_KRW로. 자산 833만↓ 계좌가 전종목 차단되던 것을 자산 대비로 완화(큰 계좌는 기존 50만 하한 불변). ⑤ adaptive 서브버그(adaptive_strategy): fallback 시 _last_update_ts 갱신(refresh 남발 방지), force_mode_override 전체필드(crisis의 max_daily_trades=0 실제 적용), get_event_loop→get_running_loop. 검증: code-verifier 통과(CONFIRMED 버그/회귀 없음, 4개 위험지점 안전), pytest 442 통과. (day_trading.py·entry_service.py는 소량 인접 선재편집 포함 — 검증 완료.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
@@ -74,6 +74,12 @@ def _today_kst() -> date:
|
||||
return datetime.now(KST).date()
|
||||
|
||||
|
||||
def _is_new_entry_allowed() -> bool:
|
||||
"""신규 진입 마감 시간(CLAUDE.md 7.3: 14:30, settings.DAY_TRADE_ENTRY_CUTOFF_HOUR/MIN)
|
||||
이전인지 확인. 청산 경로에는 영향 없음 — 신규 진입 체크에서만 사용."""
|
||||
return _kst_time() < ENTRY_CUTOFF
|
||||
|
||||
|
||||
# 감시 종목 (code → name)
|
||||
_watchlist: Dict[str, str] = {}
|
||||
|
||||
@@ -176,8 +182,8 @@ async def _get_portfolio_summary(force: bool = False) -> Dict[str, float]:
|
||||
|
||||
try:
|
||||
balance = await korea_investment_api.get_balance()
|
||||
output2 = balance.get("output2", [{}])
|
||||
summary = output2[0] if output2 else {}
|
||||
# output2가 빈 리스트([])로 오는 경우까지 방어 (get의 default는 키가 없을 때만 적용됨)
|
||||
summary = (balance.get("output2") or [{}])[0]
|
||||
total_assets = float(summary.get("tot_evlu_amt", 0) or 0)
|
||||
available_cash = float(summary.get("prvs_rcdl_excc_amt", 0) or 0)
|
||||
_portfolio_summary_cache = {"total_assets": total_assets, "available_cash": available_cash}
|
||||
@@ -339,6 +345,11 @@ async def run_day_trading_cycle() -> Dict[str, Any]:
|
||||
result["phase"] = "active"
|
||||
return result
|
||||
|
||||
if not _is_new_entry_allowed():
|
||||
# 14:30(DAY_TRADE_ENTRY_CUTOFF) 이후 — 신규 진입 마감 (CLAUDE.md 7.3), 청산은 위에서 이미 처리됨
|
||||
result["phase"] = "entry_cutoff"
|
||||
return result
|
||||
|
||||
if len(_positions) < MAX_POSITIONS and settings.AUTO_TRADE_ENABLED:
|
||||
portfolio = await _get_portfolio_summary()
|
||||
total_assets = portfolio["total_assets"]
|
||||
@@ -846,6 +857,8 @@ async def execute_day_trade(req: ExecuteRequest):
|
||||
raise HTTPException(400, f"최대 포지션 수({MAX_POSITIONS}종목)에 도달했습니다.")
|
||||
if not is_day_trade_entry_window():
|
||||
raise HTTPException(400, "데이트레이딩 진입 시간이 아닙니다 (09:30~15:00).")
|
||||
if not _is_new_entry_allowed():
|
||||
raise HTTPException(400, f"신규 진입 마감 시간({ENTRY_CUTOFF.strftime('%H:%M')}) 이후에는 진입할 수 없습니다.")
|
||||
|
||||
portfolio = await _get_portfolio_summary()
|
||||
total_assets = portfolio["total_assets"]
|
||||
|
||||
Reference in New Issue
Block a user