feat: 안전형 스윙 개선(서킷브레이커·own_trend) + 매수 현금해결 방어

퀀트 리서치(공정 유니버스+walk-forward)로 검증된 안전 게이트 2종과
매수 경로 현금 해결 방어 코드 추가. 모두 기존 시그니처 유지(§4).

- 지수 서킷브레이커: KODEX200 종가 < MA20이면 신규 스윙 진입 전면 중단
  (하락장 상관붕괴=2024H2형 방어). execute_swing_entries가 기존
  check_market_regime의 drop_from_ma20을 재사용(추가 API 없음). 백테스트
  루프에도 동일 적용. 검증: 최근1년 Sharpe 0.82→1.13, CAGR 7.2→8.6%, MDD 개선.
- own_trend 게이트: 평균회귀·눌림목 매수 시 종목 MA20 >= MA60×0.97일 때만
  진입(하락추세 물타기 방지). 실서비스는 recommendations.is_mean_reversion_candidate
  에서 StockAnalysis.ma20/ma60로, check_buy_signal엔 ma20/ma60 선택 파라미터
  (기본 None=하위호환) 추가. 백테스트(kr_backtest_engine, run_backtest)도 평균회귀 한정.
- 매수 현금 해결 방어(trade_executor._execute_buy): 0-현금 캐시를 신뢰하지 않고
  현금<=0이면 자산과 무관하게 KIS 예수금 재조회, 실현금(>0)일 때만 캐시.
  0-현금 고착으로 수량 0이 되는 잠재 무한루프 방어.

검증: code-verifier 통과(CONFIRMED 버그 없음, None-safe, §3/§4/§5 준수),
pytest 442 통과, 6파일 컨테이너 컴파일·리로드 정상.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
2026-09-16 13:17:17 +09:00
parent a0973812fb
commit 8a8e3700db
6 changed files with 92 additions and 11 deletions

View File

@@ -501,7 +501,9 @@ async def _execute_buy(
# ── 3. 현금·자산 조회 (30초 캐시 → portfolio_cache → KIS API 직접) ─────
cached = _get_cached_account()
if cached:
# [FIX] 현금이 0인 캐시는 신뢰하지 않는다. 기존엔 available_cash=0이 캐시되면
# 그대로 재사용해 수량이 0으로 계산되어 매수 주문이 영영 안 나갔다(무한루프).
if cached and cached[0] > 0:
available_cash, total_assets = cached
else:
available_cash, total_assets = 0.0, 0.0
@@ -514,8 +516,11 @@ async def _execute_buy(
total_assets = float(summary_copy.get("total_assets", 0))
except Exception:
pass
# 2차: 캐시 미스 → KIS API 직접 조회 (잔고 0이면 미초기화로 판단)
if available_cash <= 0 and total_assets <= 0:
# 2차: 현금이 0이면 KIS API로 직접 조회.
# [FIX] 기존 조건은 (cash<=0 AND assets<=0) 이라, 평가자산은 있는데 캐시 현금만
# 0인 경우(portfolio_cache 요약이 available_cash=0을 준 케이스) KIS 재조회를
# 건너뛰어 현금 0이 고착됐다. 현금이 0이면 자산과 무관하게 실제 예수금을 읽는다.
if available_cash <= 0:
try:
balance = await korea_investment_api.get_balance()
output2 = balance.get("output2", [{}])
@@ -526,7 +531,8 @@ async def _execute_buy(
logger.info(f"[CASH_INIT] KIS API 직접 조회: cash={available_cash:,.0f} assets={total_assets:,.0f}")
except Exception as e:
logger.warning(f"[CASH_INIT] KIS 잔고 조회 실패: {e}")
if available_cash > 0 or total_assets > 0:
# [FIX] 실제 현금(>0)일 때만 캐시한다. 0 현금을 캐시하면 위 무한루프가 재발한다.
if available_cash > 0:
_set_account_cache(available_cash, total_assets)
# [이슈1 수정] TradeHistory.created_at은 UTC 저장(datetime.utcnow)이므로