전략 취약점 15건 수정 (코드 리뷰 기반)

## CRITICAL/HIGH (S1-S8)
- S1: Aggressive 모드 %B 스케일링 캡 1.3 (BB 중앙선 위 매수 방지)
- S2: ML scaler train-only fit (데이터 누수 제거)
- S3: SIDEWAYS = ADX<20 AND slope≈0 (트렌드 초기 차단 해소)
- S4: EOD 청산 실패 시 재시도 + CRITICAL 로그
- S5: 일일 손실 한도에 미실현 P&L 포함
- S6: trailing stop 고점 매 사이클 갱신
- S7: 섹터 필터 미매핑 종목 "기타" 분류 (max 3 적용)
- S8: 스캐너 RSI 기준 25→46, BB 점수 -3%~+1.5% 구간

## MEDIUM (M1-M7)
- M1: 스캐너 최소 거래량 100만→10만주
- M2: ADX 히스테리시스 18/22 (국면 플리핑 방지)
- M3: 분할매도 최소 수익 1.0%→0.5%
- M4: ORB 갭 필터 4% + 레인지 폭 0.3% 최소
- M5: 쿨다운 스트릭 부분매도 시 미변경
- M6: MDD 강제매도 loss_tracker 기록 제거 (허위 ban 방지)
- M7: RSI Wilder 스무딩으로 통일

## 인프라/보안
- 인증 누락 엔드포인트 수정 (analysis, journals)
- nginx 보안 헤더 location 블록 누락 수정
- Alembic 마이그레이션 IF NOT EXISTS 가드 추가
- SellRequest/UsOrderRequest 입력 검증 강화
- 차트 네이버 폴백 추가 (DB 데이터 없을 때)
- 서버 이전 시 자동 DB 마이그레이션 + KRX 초기 수집
- CLAUDE.md 섹션 5.1 백테스트 값 동기화 (4%→6%, -6%→-7%)
- MAX_SWING_POSITIONS 25→20 (CLAUDE.md 준수)
- mdd_manager failed_count에서 skip 종목 제외

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 11:41:52 +09:00
parent eda5d76af9
commit 5f0f839039
22 changed files with 299 additions and 132 deletions

View File

@@ -12,6 +12,7 @@ from datetime import datetime, date
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Query
from app.core.security import require_api_key
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
@@ -108,7 +109,7 @@ async def _generate_journals_task(days: int = 7) -> Dict[str, Any]:
return {"created": 0, "error": str(e)}
@router.post("/generate-journals")
@router.post("/generate-journals", dependencies=[Depends(require_api_key)])
async def generate_journals(
background_tasks: BackgroundTasks,
days: int = Query(default=7, ge=1, le=30, description="최근 N일 내 미생성 거래 대상"),
@@ -140,7 +141,7 @@ async def generate_journals(
_optimization_running: bool = False
@router.post("/optimize-strategy")
@router.post("/optimize-strategy", dependencies=[Depends(require_api_key)])
async def optimize_strategy(
background_tasks: BackgroundTasks,
days: int = Query(default=30, ge=7, le=180, description="성과 분석 기간"),

View File

@@ -7,6 +7,7 @@
- POST /generate/{trade_id}: 특정 거래에 대한 일지 즉시 생성
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from app.core.security import require_api_key
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func
from typing import Optional
@@ -92,7 +93,7 @@ async def get_journal_stats(db: AsyncSession = Depends(get_db)):
return {"total": 0, "by_mood": {}, "by_strategy": {}}
@router.post("/generate/{trade_id}")
@router.post("/generate/{trade_id}", dependencies=[Depends(require_api_key)])
async def generate_journal_for_trade_id(
trade_id: int,
db: AsyncSession = Depends(get_db),

View File

@@ -329,7 +329,27 @@ async def get_chart_data_cached(stock_code: str, period: str, limit: int) -> lis
db_data = await _fetch_chart_from_db(stock_code, min(db_limit, 1200))
if not db_data:
# DB에 데이터 없으면 샘플 반환
# DB에 데이터 없으면 네이버 차트 폴백
try:
from app.services.naver_finance import get_naver_daily_chart
naver_data = await get_naver_daily_chart(stock_code, days=limit * 2)
if naver_data:
db_data = [
{
"date": item.get("stck_bsop_date", ""),
"open": float(item.get("stck_oprc", 0)),
"high": float(item.get("stck_hgpr", 0)),
"low": float(item.get("stck_lwpr", 0)),
"close": float(item.get("stck_clpr", 0)),
"volume": int(item.get("acml_vol", 0)),
}
for item in naver_data
][-limit:]
logger.info(f"차트 네이버 폴백: {stock_code} ({len(db_data)}행)")
except Exception as e:
logger.warning(f"네이버 차트 폴백 실패 ({stock_code}): {e}")
if not db_data:
return _generate_sample_chart(stock_code, period, limit).get("data", [])
# 주/월봉 집계

View File

@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from typing import List, Optional
from decimal import Decimal
from pydantic import BaseModel
from pydantic import BaseModel, Field
import pytz
from app.core.database import get_db, AsyncSessionLocal
@@ -37,8 +37,8 @@ KST = pytz.timezone('Asia/Seoul')
# Request/Response Models
class SellRequest(BaseModel):
stock_code: str
quantity: int
price: Optional[int] = None
quantity: int = Field(gt=0, description="매도 수량 (1 이상)")
price: Optional[int] = Field(None, gt=0, description="지정가 (None이면 시장가)")
def _get_kst_now():
"""현재 한국 시간 반환 (timezone-naive datetime)"""

View File

@@ -6,8 +6,8 @@
- POST /us-trading/execute: 미국 주식 매수/매도 실행
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, List
from pydantic import BaseModel, Field
from typing import Literal, Optional, List
import logging
logger = logging.getLogger(__name__)
@@ -16,8 +16,8 @@ router = APIRouter(prefix="/us-trading", tags=["US Trading"])
class UsOrderRequest(BaseModel):
ticker: str
order_type: str # "buy" or "sell"
quantity: int
order_type: Literal["buy", "sell"]
quantity: int = Field(gt=0, description="주문 수량 (1 이상)")
price: Optional[float] = None # None = market order attempt (limit 근사)