feat: 리스크 관리 강화 — 포트폴리오 락, MDD 단기전략 비활성화, 상관관계 필터
- portfolio.py: summary_lock으로 동시성 보호, apply_buy/apply_sell async화 - mdd_manager.py: MDD -10% 시 단기전략 비활성화 (is_short_term_strategy_paused) - loss_tracker.py: DB 직접 접근 → db_worker 큐 기반 전환 - db_worker.py: upsert_stock_ban 태스크 추가 - correlation_filter.py: 섹터 집중도 + 피어슨 상관관계 검증 (CLAUDE.md 5.4) - trade_executor.py: 상관관계 필터 통합, await 적용 - mean_reversion_strategy.py: config 값 타입 안전성 (float 변환) - main.py/day_trading.py: MDD 체크 후 자동매매/데이트레이딩 차단 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ from app.services.technical_indicators import (
|
||||
TREND_FILTER_ENABLED, TREND_MA_SHORT, TREND_MA_LONG, TREND_TOLERANCE_PCT, _ma, _ema,
|
||||
)
|
||||
from app.services.signal_types import BUY_KEYWORDS, SELL_KEYWORDS, STRONG_BUY_KEYWORDS, BLOCK_KEYWORDS
|
||||
from app.services.correlation_filter import validate_correlation_filter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -500,8 +501,9 @@ async def _execute_buy(
|
||||
else:
|
||||
try:
|
||||
from app.api.endpoints.portfolio import portfolio_cache
|
||||
available_cash = float(portfolio_cache.summary.get("available_cash", 0)) if portfolio_cache.summary else 0.0
|
||||
total_assets = float(portfolio_cache.summary.get("total_assets", 0)) if portfolio_cache.summary else 0.0
|
||||
summary_copy = portfolio_cache.get_summary_copy()
|
||||
available_cash = float(summary_copy.get("available_cash", 0)) if summary_copy else 0.0
|
||||
total_assets = float(summary_copy.get("total_assets", 0)) if summary_copy else 0.0
|
||||
_set_account_cache(available_cash, total_assets)
|
||||
except Exception:
|
||||
available_cash, total_assets = 0.0, 0.0
|
||||
@@ -560,6 +562,32 @@ async def _execute_buy(
|
||||
if recent_buys and (_get_kst_now() - recent_buys[0].created_at).total_seconds() < cooldown_min * 60:
|
||||
return {"action": "HOLD", "stock": stock_name, "reason": "cooldown"}
|
||||
|
||||
# ── 6.5. 상관관계 필터 (CLAUDE.md 섹션 5.4) ──────────────────────────────────
|
||||
# 섹터 집중도 + 포트폴리오 상관계수 필터
|
||||
try:
|
||||
# 현재 포트폴리오 보유 종목 조회
|
||||
portfolio_result = await db.execute(select(Portfolio).where(Portfolio.quantity > 0))
|
||||
current_holdings = portfolio_result.scalars().all()
|
||||
|
||||
# 상관관계 필터 검증
|
||||
is_valid, filter_reason = await validate_correlation_filter(
|
||||
stock_code=stock_code,
|
||||
portfolio_holdings=current_holdings,
|
||||
max_per_sector=settings.MAX_STOCKS_PER_SECTOR if hasattr(settings, 'MAX_STOCKS_PER_SECTOR') else 3,
|
||||
correlation_threshold=settings.CORRELATION_THRESHOLD if hasattr(settings, 'CORRELATION_THRESHOLD') else 0.7,
|
||||
lookback_days=60
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
logger.info(f"[CORRELATION_FILTER] {stock_code}/{stock_name}: {filter_reason}")
|
||||
return {"action": "HOLD", "stock": stock_name, "reason": filter_reason}
|
||||
|
||||
logger.debug(f"[CORRELATION_FILTER] {stock_code}/{stock_name}: {filter_reason}")
|
||||
except Exception as e:
|
||||
logger.error(f"[CORRELATION_FILTER_ERROR] {stock_code}: {type(e).__name__}: {e}")
|
||||
# 필터 오류 시 진입 거부하지 않음 (데이터 부족 시 정상 동작)
|
||||
pass
|
||||
|
||||
# ── 7. 포지션 사이징 (Kelly 우선, v2 fallback) ────────────────────────────
|
||||
# 우선순위: Half-Kelly → calculate_position_size_v2 → available_cash×4%
|
||||
current_value = float(holding.quantity * current_price) if holding else 0.0
|
||||
@@ -642,7 +670,7 @@ async def _execute_buy(
|
||||
# 인메모리 포트폴리오 캐시 즉시 반영 (UI 실시간 업데이트)
|
||||
try:
|
||||
from app.api.endpoints.portfolio import portfolio_cache
|
||||
portfolio_cache.apply_buy(stock_code, stock_name, quantity, current_price)
|
||||
await portfolio_cache.apply_buy(stock_code, stock_name, quantity, current_price)
|
||||
except Exception:
|
||||
pass
|
||||
# 3초 후 KIS 체결 확인 → 슬리피지 기록 + 부분체결 재주문 (백그라운드)
|
||||
@@ -801,7 +829,7 @@ async def _execute_sell(
|
||||
try:
|
||||
from app.api.endpoints.portfolio import portfolio_cache
|
||||
avg = float(holding.average_price or current_price)
|
||||
portfolio_cache.apply_sell(stock_code, quantity, current_price, avg)
|
||||
await portfolio_cache.apply_sell(stock_code, quantity, current_price, avg)
|
||||
portfolio_cache.holdings = [
|
||||
h for h in portfolio_cache.holdings if h.stock_code != stock_code
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user