feat: 전략 파라미터 DB 관리 시스템 구현

[추가 파일]
1. models/models.py — StrategyConfig 테이블 추가
2. services/strategy_config_service.py — DB 읽기 + TTL 캐시 서비스
3. api/endpoints/strategy_config.py — CRUD REST API
4. api/__init__.py — strategy_config 라우터 등록

[동작 방식]
- DB에 파라미터 있으면 DB 우선, 없으면 config.py 기본값 fallback
- TTL 30초 인메모리 캐시 → 재시작 없이 실시간 파라미터 변경
- 전략 코드에서 strategy_cfg.get("KEY", default) 패턴으로 사용
- /api/strategy-config 엔드포인트로 UI에서 관리 가능

[관리 대상 파라미터 그룹]
- INDICATOR: RSI, 볼린저, MA, ATR, ADX
- RISK: 손절/익절, MDD, 포지션 크기, 연속손실
- FILTER: 과열필터, 하락장필터, 트렌드필터
- DAY_TRADE: 데이트레이딩 세부 파라미터
- MEAN_REVERSION: 평균회귀 전략 파라미터
This commit is contained in:
2026-03-03 10:40:02 +09:00
parent 14e6d87025
commit f3323bb742
4 changed files with 556 additions and 61 deletions

View File

@@ -5,6 +5,7 @@ SQLAlchemy ORM 모델 정의
- Portfolio: 포트폴리오 보유 종목
- TradeHistory: 주문/체결 이력
- TradingRule: 매매 규칙 설정
- StrategyConfig: 전략 파라미터 DB 관리 (하드코딩 대체)
"""
from datetime import datetime, date
from typing import Optional, List
@@ -40,23 +41,15 @@ class TradingSession(Base):
id = Column(Integer, primary_key=True, index=True)
session_date = Column(Date, default=date.today, unique=True, index=True)
# 연속 손실 추적
consecutive_losses = Column(Integer, default=0)
consecutive_wins = Column(Integer, default=0)
# 일일 손익
daily_profit_loss = Column(Numeric(15, 2), default=0)
daily_trades_count = Column(Integer, default=0)
# 리스크 제한 상태
is_paused = Column(Boolean, default=False) # 3연패 또는 일일 손실 한도 도달 시 True
is_paused = Column(Boolean, default=False)
pause_reason = Column(String(200), nullable=True)
# 계좌 정보
starting_balance = Column(Numeric(15, 2))
ending_balance = Column(Numeric(15, 2))
peak_balance = Column(Numeric(15, 2)) # 당일 최고 잔고
peak_balance = Column(Numeric(15, 2))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -76,6 +69,52 @@ class TradingRule(Base):
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class StrategyConfig(Base):
"""전략 파라미터 DB 관리 테이블
config.py의 하드코딩된 전략 파라미터를 DB에서 관리.
- 재시작 없이 파라미터 실시간 변경 가능
- DB 값 없으면 config.py 기본값 자동 fallback
- strategy_config_service.get() 으로 접근
[그룹 분류]
- INDICATOR : RSI, 볼린저, MA, ATR, ADX 등 기술적 지표 파라미터
- RISK : 손절/익절, MDD, 포지션 크기, 연속손실 등 리스크 파라미터
- FILTER : 과열필터, 하락장필터, 트렌드필터 등 매매 필터
- DAY_TRADE : 데이트레이딩 세부 파라미터
- MEAN_REVERSION : 평균회귀 전략 파라미터
- GENERAL : 기타 일반 파라미터
"""
__tablename__ = "strategy_configs"
__table_args__ = (
UniqueConstraint('key', name='uq_strategy_config_key'),
)
id = Column(Integer, primary_key=True, index=True)
# 파라미터 식별
key = Column(String(100), nullable=False, unique=True, index=True) # ex: RSI_PERIOD
group = Column(String(50), nullable=False, index=True) # INDICATOR / RISK / FILTER / DAY_TRADE / MEAN_REVERSION / GENERAL
label = Column(String(200), nullable=False) # UI 표시 이름 (한글 가능)
description = Column(Text, nullable=True) # 파라미터 설명
# 값 관리
value_type = Column(String(20), nullable=False) # int / float / bool / str
value = Column(String(500), nullable=False) # 실제 값 (문자열로 저장, 타입 캐스팅은 서비스 레이어)
default_value = Column(String(500), nullable=False) # config.py 기본값 (참조용)
# 유효성 검증
min_value = Column(String(100), nullable=True) # 최솟값 (선택)
max_value = Column(String(100), nullable=True) # 최댓값 (선택)
# 상태
is_active = Column(Boolean, default=True, nullable=False) # False면 기본값 사용
updated_by = Column(String(100), nullable=True) # 마지막 수정자
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class TradeHistory(Base):
__tablename__ = "trade_history"
@@ -88,21 +127,18 @@ class TradeHistory(Base):
order_quantity = Column(Integer, nullable=False)
executed_price = Column(Numeric(15, 2))
executed_quantity = Column(Integer)
total_amount = Column(Numeric(15, 2)) # 총 거래 금액
total_amount = Column(Numeric(15, 2))
status = Column(String(50), nullable=False)
order_id = Column(String(100))
message = Column(Text)
entry_reason = Column(Text) # 진입 이유 (매수 신호 상세)
exit_reason = Column(Text) # 매도 이유 (매도 신호 상세)
signals = Column(JSONB) # 기술적 신호 목록
# 🔥 리스크 관리 필드 추가
entry_trade_id = Column(Integer, nullable=True) # 매도 시 얰결된 매수 거래 ID
slippage = Column(Numeric(15, 2), nullable=True) # 슬리피지 (체결가 - 주문가)
commission = Column(Numeric(15, 2), nullable=True) # 수수료
net_profit_loss = Column(Numeric(15, 2), nullable=True) # 수수료 차감 후 순손익
profit_loss_rate = Column(Numeric(10, 4), nullable=True) # 수익률 (%)
entry_reason = Column(Text)
exit_reason = Column(Text)
signals = Column(JSONB)
entry_trade_id = Column(Integer, nullable=True)
slippage = Column(Numeric(15, 2), nullable=True)
commission = Column(Numeric(15, 2), nullable=True)
net_profit_loss = Column(Numeric(15, 2), nullable=True)
profit_loss_rate = Column(Numeric(10, 4), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
executed_at = Column(DateTime)
@@ -118,7 +154,7 @@ class Portfolio(Base):
current_price = Column(Numeric(15, 2))
profit_loss = Column(Numeric(15, 2))
profit_loss_rate = Column(Numeric(10, 4))
market = Column(String(10), nullable=True, default="KRX") # KRX, NASD, NYSE, AMEX
market = Column(String(10), nullable=True, default="KRX")
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -129,7 +165,6 @@ class MarketData(Base):
stock_code = Column(String(20), nullable=False, index=True)
price = Column(Numeric(15, 2), nullable=False)
change_rate = Column(Numeric(10, 4))
# real‑time volume may also exceed int32; switch to bigint for safety
volume = Column(BigInteger)
high_price = Column(Numeric(15, 2))
low_price = Column(Numeric(15, 2))
@@ -188,14 +223,9 @@ class StockAnalysis(Base):
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100), nullable=False)
analysis_date = Column(Date, nullable=False, index=True)
# 가격 정보
current_price = Column(Integer, nullable=False)
change_rate = Column(Numeric(10, 4))
# volumes can exceed 32‑bit limits (e.g. 3 billion+), use bigint to avoid overflow
volume = Column(BigInteger)
# 기술적 지표
rsi = Column(Numeric(10, 4))
ma5 = Column(Numeric(15, 2))
ma20 = Column(Numeric(15, 2))
@@ -205,23 +235,13 @@ class StockAnalysis(Base):
bollinger_upper = Column(Numeric(15, 2))
bollinger_lower = Column(Numeric(15, 2))
bollinger_middle = Column(Numeric(15, 2))
# 분석 결과
signals = Column(JSONB) # ["RSI 과매도", "골든크로스" 등]
signals = Column(JSONB)
score = Column(Integer, default=0)
# ATR (섹션 6.2 ATR 기반 손절)
atr_14 = Column(Numeric(15, 2), nullable=True)
# ADX (추세 강도 — 평균회귀 필터용)
adx_14 = Column(Numeric(10, 2), nullable=True)
# ML 앙상블 스코어
ml_score = Column(Integer, nullable=True) # 앙상블 스코어 (0~100)
ml_confidence = Column(Numeric(5, 4), nullable=True) # 신뢰도 (0~1)
ml_signal = Column(String(10), nullable=True) # BUY/HOLD/SELL
# 메타데이터
ml_score = Column(Integer, nullable=True)
ml_confidence = Column(Numeric(5, 4), nullable=True)
ml_signal = Column(String(10), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -242,11 +262,10 @@ class ActiveTrade(Base):
take_profit = Column(Numeric(15, 2))
order_id = Column(String(100))
entry_reason = Column(Text)
status = Column(String(20), default="active") # active, closed
# 분할 청산 지원 (섹션 6.2, 7.3)
remaining_pct = Column(Numeric(5, 2), default=1.0) # 잔여 비율 (1.0=전량, 0.5=절반 청산 후)
highest_since_entry = Column(Numeric(15, 2), nullable=True) # 진입 후 최고가 (트레일링 스탑용)
strategy_name = Column(String(100), nullable=True) # 전략명 (MEAN_REVERSION 등)
status = Column(String(20), default="active")
remaining_pct = Column(Numeric(5, 2), default=1.0)
highest_since_entry = Column(Numeric(15, 2), nullable=True)
strategy_name = Column(String(100), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -255,7 +274,7 @@ class TradeJournal(Base):
__tablename__ = "trade_journals"
id = Column(Integer, primary_key=True, index=True)
trade_id = Column(Integer) # ForeignKey("trade_history.id") - models.py 내 순환 참조 방지 위해 정수로 정의
trade_id = Column(Integer)
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100))
journal_date = Column(DateTime, default=datetime.utcnow)
@@ -269,24 +288,15 @@ class TradeJournal(Base):
class StockBan(Base):
"""종목별 연속 손실 금지 기록 (CLAUDE.md 섹션 7.4)
재시작 후에도 ban 상태 유지를 위해 DB에 영속화.
"""
__tablename__ = "stock_bans"
stock_code = Column(String(20), primary_key=True)
consecutive_losses = Column(Integer, default=0, nullable=False)
ban_until = Column(DateTime, nullable=True) # None이면 금지 아님
ban_until = Column(DateTime, nullable=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class WatchlistStock(Base):
"""분석종목 — 장중 미니배치가 관리하는 활성 관심 종목.
아침 배치(batch_daily_analysis)가 score 상위 50개로 초기화하고,
장중 미니배치(batch_intraday_analysis)가 10분마다 진입/제거를 관리한다.
"""
__tablename__ = "watchlist_stocks"
id = Column(Integer, primary_key=True, index=True)
@@ -301,7 +311,6 @@ class WatchlistStock(Base):
class WatchlistHistory(Base):
"""분석종목 스코어 히스토리 — 10분마다 적재되는 시계열 데이터."""
__tablename__ = "watchlist_history"
id = Column(Integer, primary_key=True, index=True)