316 lines
12 KiB
Python
316 lines
12 KiB
Python
"""
|
||
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))
|
||
market = Column(String(10), nullable=True, default="KRX") # KRX, NASD, NYSE, AMEX
|
||
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)
|