""" SQLAlchemy ORM 모델 정의 - StockDailyPrice: 일별 주가 데이터 - Portfolio: 포트폴리오 보유 종목 - TradeHistory: 주문/체결 이력 - TradingRule: 매매 규칙 설정 """ 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) # 3연패 또는 일일 손실 한도 도달 시 True 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 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) # 매도 시 얰결된 매수 거래 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) # 수익률 (%) 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)) 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)) # 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)) 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)) # 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)) 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) # ["RSI 과매도", "골든크로스" 등] 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 # 메타데이터 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") # 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 등) 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) # ForeignKey("trade_history.id") - models.py 내 순환 참조 방지 위해 정수로 정의 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): """종목별 연속 손실 금지 기록 (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이면 금지 아님 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) 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): """분석종목 스코어 히스토리 — 10분마다 적재되는 시계열 데이터.""" __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)