diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e69de29..2ca74fd 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -0,0 +1,47 @@ +from app.models.user import User +from app.models.portfolio import ( + Portfolio, + PortfolioType, + Target, + Holding, + Transaction, + TransactionType, + PortfolioSnapshot, + SnapshotHolding, +) +from app.models.stock import ( + Stock, + StockType, + Sector, + Valuation, + Price, + Financial, + ReportType, + ETF, + ETFPrice, + AssetClass, + JobLog, +) + +__all__ = [ + "User", + "Portfolio", + "PortfolioType", + "Target", + "Holding", + "Transaction", + "TransactionType", + "PortfolioSnapshot", + "SnapshotHolding", + "Stock", + "StockType", + "Sector", + "Valuation", + "Price", + "Financial", + "ReportType", + "ETF", + "ETFPrice", + "AssetClass", + "JobLog", +] diff --git a/backend/app/models/portfolio.py b/backend/app/models/portfolio.py new file mode 100644 index 0000000..f111fa8 --- /dev/null +++ b/backend/app/models/portfolio.py @@ -0,0 +1,99 @@ +""" +Portfolio related models. +""" +from datetime import datetime +from sqlalchemy import ( + Column, Integer, String, Numeric, DateTime, Date, + ForeignKey, Text, Enum as SQLEnum +) +from sqlalchemy.orm import relationship +import enum + +from app.core.database import Base + + +class PortfolioType(str, enum.Enum): + PENSION = "pension" + GENERAL = "general" + + +class TransactionType(str, enum.Enum): + BUY = "buy" + SELL = "sell" + + +class Portfolio(Base): + __tablename__ = "portfolios" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + name = Column(String(100), nullable=False) + portfolio_type = Column(SQLEnum(PortfolioType), default=PortfolioType.GENERAL) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + targets = relationship("Target", back_populates="portfolio", cascade="all, delete-orphan") + holdings = relationship("Holding", back_populates="portfolio", cascade="all, delete-orphan") + transactions = relationship("Transaction", back_populates="portfolio", cascade="all, delete-orphan") + snapshots = relationship("PortfolioSnapshot", back_populates="portfolio", cascade="all, delete-orphan") + + +class Target(Base): + __tablename__ = "targets" + + portfolio_id = Column(Integer, ForeignKey("portfolios.id"), primary_key=True) + ticker = Column(String(20), primary_key=True) + target_ratio = Column(Numeric(5, 2), nullable=False) + + portfolio = relationship("Portfolio", back_populates="targets") + + +class Holding(Base): + __tablename__ = "holdings" + + portfolio_id = Column(Integer, ForeignKey("portfolios.id"), primary_key=True) + ticker = Column(String(20), primary_key=True) + quantity = Column(Integer, nullable=False, default=0) + avg_price = Column(Numeric(12, 2), nullable=False, default=0) + + portfolio = relationship("Portfolio", back_populates="holdings") + + +class Transaction(Base): + __tablename__ = "transactions" + + id = Column(Integer, primary_key=True, index=True) + portfolio_id = Column(Integer, ForeignKey("portfolios.id"), nullable=False) + ticker = Column(String(20), nullable=False) + tx_type = Column(SQLEnum(TransactionType), nullable=False) + quantity = Column(Integer, nullable=False) + price = Column(Numeric(12, 2), nullable=False) + executed_at = Column(DateTime, nullable=False) + memo = Column(Text, nullable=True) + + portfolio = relationship("Portfolio", back_populates="transactions") + + +class PortfolioSnapshot(Base): + __tablename__ = "portfolio_snapshots" + + id = Column(Integer, primary_key=True, index=True) + portfolio_id = Column(Integer, ForeignKey("portfolios.id"), nullable=False) + total_value = Column(Numeric(15, 2), nullable=False) + snapshot_date = Column(Date, nullable=False) + + portfolio = relationship("Portfolio", back_populates="snapshots") + holdings = relationship("SnapshotHolding", back_populates="snapshot", cascade="all, delete-orphan") + + +class SnapshotHolding(Base): + __tablename__ = "snapshot_holdings" + + snapshot_id = Column(Integer, ForeignKey("portfolio_snapshots.id"), primary_key=True) + ticker = Column(String(20), primary_key=True) + quantity = Column(Integer, nullable=False) + price = Column(Numeric(12, 2), nullable=False) + value = Column(Numeric(15, 2), nullable=False) + current_ratio = Column(Numeric(5, 2), nullable=False) + + snapshot = relationship("PortfolioSnapshot", back_populates="holdings") diff --git a/backend/app/models/stock.py b/backend/app/models/stock.py new file mode 100644 index 0000000..af39787 --- /dev/null +++ b/backend/app/models/stock.py @@ -0,0 +1,123 @@ +""" +Stock and market data models. +""" +from datetime import datetime +from sqlalchemy import ( + Column, Integer, String, Numeric, DateTime, Date, + BigInteger, Text, Enum as SQLEnum +) +import enum + +from app.core.database import Base + + +class StockType(str, enum.Enum): + COMMON = "common" + SPAC = "spac" + PREFERRED = "preferred" + REIT = "reit" + OTHER = "other" + + +class ReportType(str, enum.Enum): + ANNUAL = "annual" + QUARTERLY = "quarterly" + + +class AssetClass(str, enum.Enum): + EQUITY = "equity" + BOND = "bond" + GOLD = "gold" + MIXED = "mixed" + + +class Stock(Base): + __tablename__ = "stocks" + + ticker = Column(String(20), primary_key=True) + name = Column(String(100), nullable=False) + market = Column(String(20), nullable=False) + close_price = Column(Numeric(12, 2), nullable=True) + market_cap = Column(BigInteger, nullable=True) + eps = Column(Numeric(12, 2), nullable=True) + forward_eps = Column(Numeric(12, 2), nullable=True) + bps = Column(Numeric(12, 2), nullable=True) + dividend_per_share = Column(Numeric(12, 2), nullable=True) + stock_type = Column(SQLEnum(StockType), default=StockType.COMMON) + base_date = Column(Date, nullable=False) + + +class Sector(Base): + __tablename__ = "sectors" + + ticker = Column(String(20), primary_key=True) + sector_code = Column(String(10), nullable=False) + company_name = Column(String(100), nullable=False) + sector_name = Column(String(50), nullable=False) + base_date = Column(Date, nullable=False) + + +class Valuation(Base): + __tablename__ = "valuations" + + ticker = Column(String(20), primary_key=True) + base_date = Column(Date, primary_key=True) + per = Column(Numeric(10, 2), nullable=True) + pbr = Column(Numeric(10, 2), nullable=True) + psr = Column(Numeric(10, 2), nullable=True) + pcr = Column(Numeric(10, 2), nullable=True) + dividend_yield = Column(Numeric(6, 2), nullable=True) + + +class Price(Base): + __tablename__ = "prices" + + ticker = Column(String(20), primary_key=True) + date = Column(Date, primary_key=True) + open = Column(Numeric(12, 2), nullable=False) + high = Column(Numeric(12, 2), nullable=False) + low = Column(Numeric(12, 2), nullable=False) + close = Column(Numeric(12, 2), nullable=False) + volume = Column(BigInteger, nullable=False) + + +class Financial(Base): + __tablename__ = "financials" + + ticker = Column(String(20), primary_key=True) + base_date = Column(Date, primary_key=True) + report_type = Column(SQLEnum(ReportType), primary_key=True) + account = Column(String(50), primary_key=True) + value = Column(Numeric(20, 2), nullable=True) + + +class ETF(Base): + __tablename__ = "etfs" + + ticker = Column(String(20), primary_key=True) + name = Column(String(100), nullable=False) + asset_class = Column(SQLEnum(AssetClass), nullable=False) + market = Column(String(20), nullable=False) + expense_ratio = Column(Numeric(5, 4), nullable=True) + + +class ETFPrice(Base): + __tablename__ = "etf_prices" + + ticker = Column(String(20), primary_key=True) + date = Column(Date, primary_key=True) + close = Column(Numeric(12, 2), nullable=False) + nav = Column(Numeric(12, 2), nullable=True) + volume = Column(BigInteger, nullable=True) + + +class JobLog(Base): + __tablename__ = "job_logs" + + id = Column(Integer, primary_key=True, index=True) + job_name = Column(String(50), nullable=False) + status = Column(String(20), nullable=False) + started_at = Column(DateTime, default=datetime.utcnow) + finished_at = Column(DateTime, nullable=True) + records_count = Column(Integer, nullable=True) + error_msg = Column(Text, nullable=True) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..725c06d --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,17 @@ +""" +User model for authentication. +""" +from datetime import datetime +from sqlalchemy import Column, Integer, String, DateTime +from app.core.database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, index=True, nullable=False) + email = Column(String(100), unique=True, index=True, nullable=False) + hashed_password = Column(String(255), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)