Files
auto-stock/backend/app/models/models.py
shuim 2baf06a5fd feat: 매매일지 자동 생성 + 전략 자동 최적화 시스템 추가
- trade_analyzer.py: 거래 성과 분석(승률/손익비/전략별 패턴), 템플릿 기반 일지 자동 생성
- strategy_optimizer.py: 그리드 서치 기반 파라미터 최적화, Sharpe 10% 개선 시 자동 적용
- analysis.py: /performance /insights /generate-journals /optimize-strategy /optimization-history 엔드포인트
- journals.py: POST /generate/{trade_id}, GET /stats 엔드포인트 추가
- models.py: StrategyOptimizationLog 테이블 추가
- main.py: post_market_analyzer_scheduler (16:00 KST 자동 실행)
- frontend: Analysis 페이지 추가 (성과 요약/인사이트/일지 생성/최적화 이력)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:28:10 +09:00

345 lines
14 KiB
Python

"""
SQLAlchemy ORM 모델 정의
- StockDailyPrice: 일별 주가 데이터
- Portfolio: 포트폴리오 보유 종목
- TradeHistory: 주문/체결 이력
- TradingRule: 매매 규칙 설정
- StrategyConfig: 전략 파라미터 DB 관리 (하드코딩 대체)
"""
from datetime import datetime, date
from typing import Optional, List
from sqlalchemy import Column, Integer, BigInteger, String, Numeric, Boolean, DateTime, Text, ARRAY, Date, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB
from app.core.database import Base
class StockDailyPrice(Base):
"""KRX 일봉 히스토리컬 데이터 (pykrx 수집)"""
__tablename__ = "stock_daily_prices"
__table_args__ = (
UniqueConstraint('stock_code', 'trade_date', name='uq_stock_daily'),
)
id = Column(Integer, primary_key=True, index=True)
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100))
trade_date = Column(Date, nullable=False, index=True)
open_price = Column(Integer)
high_price = Column(Integer)
low_price = Column(Integer)
close_price = Column(Integer)
volume = Column(BigInteger)
change_rate = Column(Numeric(10, 4))
market_cap = Column(BigInteger, nullable=True)
class TradingSession(Base):
"""🔥 일일 트레이딩 세션 관리 (리스크 관리)"""
__tablename__ = "trading_sessions"
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)
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))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class TradingRule(Base):
__tablename__ = "trading_rules"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False)
description = Column(Text)
rule_type = Column(String(50), nullable=False)
conditions = Column(JSONB, nullable=False)
actions = Column(JSONB, nullable=False)
is_active = Column(Boolean, default=True)
priority = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
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"
id = Column(Integer, primary_key=True, index=True)
rule_id = Column(Integer)
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100))
order_type = Column(String(20), nullable=False)
order_price = Column(Numeric(15, 2))
order_quantity = Column(Integer, nullable=False)
executed_price = Column(Numeric(15, 2))
executed_quantity = Column(Integer)
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)
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)
class Portfolio(Base):
__tablename__ = "portfolio"
id = Column(Integer, primary_key=True, index=True)
stock_code = Column(String(20), nullable=False, unique=True)
stock_name = Column(String(100))
quantity = Column(Integer, nullable=False, default=0)
average_price = Column(Numeric(15, 2))
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")
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class MarketData(Base):
__tablename__ = "market_data"
id = Column(Integer, primary_key=True, index=True)
stock_code = Column(String(20), nullable=False, index=True)
price = Column(Numeric(15, 2), nullable=False)
change_rate = Column(Numeric(10, 4))
volume = Column(BigInteger)
high_price = Column(Numeric(15, 2))
low_price = Column(Numeric(15, 2))
open_price = Column(Numeric(15, 2))
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
class News(Base):
__tablename__ = "news"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(500), nullable=False)
content = Column(Text)
source = Column(String(100))
url = Column(Text)
stock_codes = Column(ARRAY(String))
sentiment = Column(String(20))
published_at = Column(DateTime, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
class ClaudeAnalysisLog(Base):
__tablename__ = "claude_analysis_log"
id = Column(Integer, primary_key=True, index=True)
rule_id = Column(Integer)
stock_code = Column(String(20))
analysis_type = Column(String(50))
input_data = Column(JSONB)
analysis_result = Column(JSONB)
decision = Column(String(50))
confidence = Column(Numeric(5, 4))
created_at = Column(DateTime, default=datetime.utcnow, index=True)
class MCPToolCall(Base):
__tablename__ = "mcp_tool_calls"
id = Column(Integer, primary_key=True, index=True)
tool_name = Column(String(100), nullable=False)
parameters = Column(JSONB)
result = Column(JSONB)
status = Column(String(50))
execution_time_ms = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
class StockAnalysis(Base):
"""매일 배치로 저장되는 종목 분석 데이터"""
__tablename__ = "stock_analysis"
__table_args__ = (
UniqueConstraint("stock_code", "analysis_date", name="uq_stock_analysis_code_date"),
)
id = Column(Integer, primary_key=True, index=True)
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))
volume = Column(BigInteger)
rsi = Column(Numeric(10, 4))
ma5 = Column(Numeric(15, 2))
ma20 = Column(Numeric(15, 2))
ma60 = Column(Numeric(15, 2))
stochastic_k = Column(Numeric(10, 4))
stochastic_d = Column(Numeric(10, 4))
bollinger_upper = Column(Numeric(15, 2))
bollinger_lower = Column(Numeric(15, 2))
bollinger_middle = Column(Numeric(15, 2))
signals = Column(JSONB)
score = Column(Integer, default=0)
atr_14 = Column(Numeric(15, 2), nullable=True)
adx_14 = Column(Numeric(10, 2), nullable=True)
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)
class ActiveTrade(Base):
"""현재 활성화된(보유 중인) 거래를 추적하기 위한 모델"""
__tablename__ = "active_trades"
id = Column(Integer, primary_key=True, index=True)
stock_code = Column(String(20), nullable=False)
stock_name = Column(String(100))
quantity = Column(Integer, nullable=False)
entry_price = Column(Numeric(15, 2), nullable=False)
current_price = Column(Numeric(15, 2))
profit_loss = Column(Numeric(15, 2))
profit_loss_rate = Column(Numeric(10, 4))
stop_loss = Column(Numeric(15, 2))
take_profit = Column(Numeric(15, 2))
order_id = Column(String(100))
entry_reason = Column(Text)
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)
class TradeJournal(Base):
__tablename__ = "trade_journals"
id = Column(Integer, primary_key=True, index=True)
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)
content = Column(Text)
strategy_name = Column(String(100))
mood = Column(String(50))
tags = Column(JSONB)
screenshot_url = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class StockBan(Base):
__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)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class WatchlistStock(Base):
__tablename__ = "watchlist_stocks"
id = Column(Integer, primary_key=True, index=True)
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100), nullable=False)
added_at = Column(DateTime, nullable=False)
added_reason = Column(String(50), nullable=False, default="morning_batch")
score_at_add = Column(Integer, nullable=True)
is_active = Column(Boolean, default=True, nullable=False, index=True)
removed_at = Column(DateTime, nullable=True)
removed_reason = Column(String(50), nullable=True)
class WatchlistHistory(Base):
__tablename__ = "watchlist_history"
id = Column(Integer, primary_key=True, index=True)
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100), nullable=False)
recorded_at = Column(DateTime, nullable=False, index=True)
score = Column(Integer, nullable=True)
current_price = Column(Numeric(15, 2), nullable=True)
change_rate = Column(Numeric(10, 4), nullable=True)
rsi = Column(Numeric(10, 4), nullable=True)
signals = Column(JSONB, nullable=True)
class StrategyOptimizationLog(Base):
"""전략 자동 최적화 실행 이력"""
__tablename__ = "strategy_optimization_logs"
id = Column(Integer, primary_key=True, index=True)
run_date = Column(Date, nullable=False, index=True)
trigger = Column(String(20), nullable=False, default="auto") # auto / manual
period_days = Column(Integer, nullable=False, default=90)
best_params = Column(JSONB, nullable=True)
best_sharpe = Column(Numeric(10, 4), nullable=True)
best_win_rate = Column(Numeric(10, 4), nullable=True)
best_mdd = Column(Numeric(10, 4), nullable=True)
current_sharpe = Column(Numeric(10, 4), nullable=True)
previous_params = Column(JSONB, nullable=True)
applied = Column(Boolean, default=False, nullable=False)
apply_reason = Column(Text, nullable=True)
total_combinations = Column(Integer, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, index=True)