diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 2ca74fd..0926640 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -22,6 +22,15 @@ from app.models.stock import ( AssetClass, JobLog, ) +from app.models.backtest import ( + Backtest, + BacktestStatus, + RebalancePeriod, + BacktestResult, + BacktestEquityCurve, + BacktestHolding, + BacktestTransaction, +) __all__ = [ "User", @@ -44,4 +53,11 @@ __all__ = [ "ETFPrice", "AssetClass", "JobLog", + "Backtest", + "BacktestStatus", + "RebalancePeriod", + "BacktestResult", + "BacktestEquityCurve", + "BacktestHolding", + "BacktestTransaction", ] diff --git a/backend/app/models/backtest.py b/backend/app/models/backtest.py new file mode 100644 index 0000000..76ba228 --- /dev/null +++ b/backend/app/models/backtest.py @@ -0,0 +1,109 @@ +""" +Backtest related models. +""" +from datetime import datetime +from sqlalchemy import ( + Column, Integer, String, Numeric, DateTime, Date, + Text, ForeignKey, JSON, Enum as SQLEnum +) +from sqlalchemy.orm import relationship +import enum + +from app.core.database import Base + + +class BacktestStatus(str, enum.Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + + +class RebalancePeriod(str, enum.Enum): + MONTHLY = "monthly" + QUARTERLY = "quarterly" + SEMI_ANNUAL = "semi_annual" + ANNUAL = "annual" + + +class Backtest(Base): + __tablename__ = "backtests" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + strategy_type = Column(String(50), nullable=False) + strategy_params = Column(JSON, default={}) + start_date = Column(Date, nullable=False) + end_date = Column(Date, nullable=False) + rebalance_period = Column(SQLEnum(RebalancePeriod), default=RebalancePeriod.QUARTERLY) + initial_capital = Column(Numeric(20, 2), nullable=False) + commission_rate = Column(Numeric(10, 6), default=0.00015) + slippage_rate = Column(Numeric(10, 6), default=0.001) + benchmark = Column(String(20), default="KOSPI") + top_n = Column(Integer, default=30) + status = Column(SQLEnum(BacktestStatus), default=BacktestStatus.PENDING) + created_at = Column(DateTime, default=datetime.utcnow) + completed_at = Column(DateTime, nullable=True) + error_message = Column(Text, nullable=True) + + # Relationships + result = relationship("BacktestResult", back_populates="backtest", uselist=False) + equity_curve = relationship("BacktestEquityCurve", back_populates="backtest") + holdings = relationship("BacktestHolding", back_populates="backtest") + transactions = relationship("BacktestTransaction", back_populates="backtest") + + +class BacktestResult(Base): + __tablename__ = "backtest_results" + + backtest_id = Column(Integer, ForeignKey("backtests.id"), primary_key=True) + total_return = Column(Numeric(10, 4)) + cagr = Column(Numeric(10, 4)) + mdd = Column(Numeric(10, 4)) + sharpe_ratio = Column(Numeric(10, 4)) + volatility = Column(Numeric(10, 4)) + benchmark_return = Column(Numeric(10, 4)) + excess_return = Column(Numeric(10, 4)) + + backtest = relationship("Backtest", back_populates="result") + + +class BacktestEquityCurve(Base): + __tablename__ = "backtest_equity_curve" + + backtest_id = Column(Integer, ForeignKey("backtests.id"), primary_key=True) + date = Column(Date, primary_key=True) + portfolio_value = Column(Numeric(20, 2)) + benchmark_value = Column(Numeric(20, 2)) + drawdown = Column(Numeric(10, 4)) + + backtest = relationship("Backtest", back_populates="equity_curve") + + +class BacktestHolding(Base): + __tablename__ = "backtest_holdings" + + backtest_id = Column(Integer, ForeignKey("backtests.id"), primary_key=True) + rebalance_date = Column(Date, primary_key=True) + ticker = Column(String(20), primary_key=True) + name = Column(String(100)) + weight = Column(Numeric(10, 4)) + shares = Column(Integer) + price = Column(Numeric(12, 2)) + + backtest = relationship("Backtest", back_populates="holdings") + + +class BacktestTransaction(Base): + __tablename__ = "backtest_transactions" + + id = Column(Integer, primary_key=True, index=True) + backtest_id = Column(Integer, ForeignKey("backtests.id"), nullable=False) + date = Column(Date, nullable=False) + ticker = Column(String(20), nullable=False) + action = Column(String(10), nullable=False) # buy/sell + shares = Column(Integer, nullable=False) + price = Column(Numeric(12, 2), nullable=False) + commission = Column(Numeric(12, 2), nullable=False) + + backtest = relationship("Backtest", back_populates="transactions")