머니페니 9249821a25 feat: add realized/unrealized PnL tracking and position sizing guide
- Add realized_pnl column to transactions table with alembic migration
- Calculate realized PnL on sell transactions: (sell_price - avg_price) * quantity
- Show total realized/unrealized PnL in portfolio detail summary cards
- Show per-transaction realized PnL in transaction history table
- Add position sizing API endpoint (GET /portfolios/{id}/position-size)
- Show position sizing guide in signal execution modal for buy signals
- 8 new E2E tests, all 88 tests passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 19:04:36 +09:00

101 lines
3.5 KiB
Python

"""
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)
realized_pnl = Column(Numeric(15, 2), 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")