전략 취약점 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

@@ -36,7 +36,7 @@ def upgrade() -> None:
]:
try:
batch_op.add_column(sa.Column(col_name, col_type, nullable=True))
except Exception:
except (sa.exc.OperationalError, sa.exc.ProgrammingError):
pass # 이미 존재하는 컬럼은 무시
# total_amount 백필

View File

@@ -11,6 +11,7 @@ Create Date: 2026-03-10
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "0002"
down_revision: Union[str, None] = "0001"
@@ -18,63 +19,26 @@ branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _safe_create_index(table, index_name, columns):
"""인덱스가 이미 존재하면 스킵."""
conn = op.get_bind()
result = conn.execute(sa.text(
"SELECT 1 FROM pg_indexes WHERE tablename=:t AND indexname=:i"
), {"t": table, "i": index_name})
if not result.fetchone():
op.create_index(index_name, table, columns)
def upgrade() -> None:
# ── trade_history 인덱스 ──────────────────────────────────────────────
with op.batch_alter_table("trade_history", schema=None) as batch_op:
batch_op.create_index(
"ix_trade_history_status_created_at",
["status", "created_at"],
)
batch_op.create_index(
"ix_trade_history_stock_order_created",
["stock_code", "order_type", "created_at"],
)
# ── market_data 인덱스 ────────────────────────────────────────────────
with op.batch_alter_table("market_data", schema=None) as batch_op:
batch_op.create_index(
"ix_market_data_stock_code_timestamp",
["stock_code", "timestamp"],
)
# ── stock_analysis 인덱스 ─────────────────────────────────────────────
with op.batch_alter_table("stock_analysis", schema=None) as batch_op:
batch_op.create_index(
"ix_stock_analysis_stock_code_analysis_date",
["stock_code", "analysis_date"],
)
# ── portfolio 인덱스 ──────────────────────────────────────────────────
with op.batch_alter_table("portfolio", schema=None) as batch_op:
batch_op.create_index(
"ix_portfolio_quantity",
["quantity"],
)
# ── active_trades 인덱스 ──────────────────────────────────────────────
with op.batch_alter_table("active_trades", schema=None) as batch_op:
batch_op.create_index(
"ix_active_trades_stock_code_status",
["stock_code", "status"],
)
batch_op.create_index(
"ix_active_trades_status",
["status"],
)
# ── watchlist_stocks 인덱스 ───────────────────────────────────────────
with op.batch_alter_table("watchlist_stocks", schema=None) as batch_op:
batch_op.create_index(
"ix_watchlist_stocks_is_active_stock_code",
["is_active", "stock_code"],
)
# ── claude_analysis_log 인덱스 ────────────────────────────────────────
with op.batch_alter_table("claude_analysis_log", schema=None) as batch_op:
batch_op.create_index(
"ix_claude_analysis_log_stock_code_created_at",
["stock_code", "created_at"],
)
_safe_create_index("trade_history", "ix_trade_history_status_created_at", ["status", "created_at"])
_safe_create_index("trade_history", "ix_trade_history_stock_order_created", ["stock_code", "order_type", "created_at"])
_safe_create_index("market_data", "ix_market_data_stock_code_timestamp", ["stock_code", "timestamp"])
_safe_create_index("stock_analysis", "ix_stock_analysis_stock_code_analysis_date", ["stock_code", "analysis_date"])
_safe_create_index("portfolio", "ix_portfolio_quantity", ["quantity"])
_safe_create_index("active_trades", "ix_active_trades_stock_code_status", ["stock_code", "status"])
_safe_create_index("active_trades", "ix_active_trades_status", ["status"])
_safe_create_index("watchlist_stocks", "ix_watchlist_stocks_is_active_stock_code", ["is_active", "stock_code"])
_safe_create_index("claude_analysis_log", "ix_claude_analysis_log_stock_code_created_at", ["stock_code", "created_at"])
def downgrade() -> None:

View File

@@ -5,6 +5,7 @@ Revises: 0004
Create Date: 2026-03-20
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = "0005"
@@ -14,8 +15,12 @@ depends_on = None
def upgrade() -> None:
# ix_active_trades_status는 0002에서 이미 생성됨 → stock_code 인덱스만 추가
op.create_index("ix_active_trades_stock_code", "active_trades", ["stock_code"])
conn = op.get_bind()
result = conn.execute(sa.text(
"SELECT 1 FROM pg_indexes WHERE tablename='active_trades' AND indexname='ix_active_trades_stock_code'"
))
if not result.fetchone():
op.create_index("ix_active_trades_stock_code", "active_trades", ["stock_code"])
def downgrade() -> None:

View File

@@ -16,10 +16,16 @@ depends_on = None
def upgrade() -> None:
op.add_column(
"news",
sa.Column("sentiment_score", sa.Numeric(5, 3), nullable=True),
)
conn = op.get_bind()
result = conn.execute(sa.text(
"SELECT 1 FROM information_schema.columns "
"WHERE table_name='news' AND column_name='sentiment_score'"
))
if not result.fetchone():
op.add_column(
"news",
sa.Column("sentiment_score", sa.Numeric(5, 3), nullable=True),
)
def downgrade() -> None: