Initial release v1.1.0

- Complete MVP for tracking Fidelity brokerage account performance
- Transaction import from CSV with deduplication
- Automatic FIFO position tracking with options support
- Real-time P&L calculations with market data caching
- Dashboard with timeframe filtering (30/90/180 days, 1 year, YTD, all time)
- Docker-based deployment with PostgreSQL backend
- React/TypeScript frontend with TailwindCSS
- FastAPI backend with SQLAlchemy ORM

Features:
- Multi-account support
- Import via CSV upload or filesystem
- Open and closed position tracking
- Balance history charting
- Performance analytics and metrics
- Top trades analysis
- Responsive UI design

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris
2026-01-22 14:27:43 -05:00
commit eea4469095
90 changed files with 14513 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
"""SQLAlchemy models for the application."""
from app.models.account import Account
from app.models.transaction import Transaction
from app.models.position import Position, PositionTransaction
from app.models.market_price import MarketPrice
__all__ = ["Account", "Transaction", "Position", "PositionTransaction", "MarketPrice"]

View File

@@ -0,0 +1,41 @@
"""Account model representing a brokerage account."""
from sqlalchemy import Column, Integer, String, DateTime, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.database import Base
class AccountType(str, enum.Enum):
"""Enumeration of account types."""
CASH = "cash"
MARGIN = "margin"
class Account(Base):
"""
Represents a brokerage account.
Attributes:
id: Primary key
account_number: Unique account identifier
account_name: Human-readable account name
account_type: Type of account (cash or margin)
created_at: Timestamp of account creation
updated_at: Timestamp of last update
transactions: Related transactions
positions: Related positions
"""
__tablename__ = "accounts"
id = Column(Integer, primary_key=True, index=True)
account_number = Column(String(50), unique=True, nullable=False, index=True)
account_name = Column(String(200), nullable=False)
account_type = Column(Enum(AccountType), nullable=False, default=AccountType.CASH)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now(), nullable=False)
# Relationships
transactions = relationship("Transaction", back_populates="account", cascade="all, delete-orphan")
positions = relationship("Position", back_populates="account", cascade="all, delete-orphan")

View File

@@ -0,0 +1,29 @@
"""Market price cache model for storing Yahoo Finance data."""
from sqlalchemy import Column, Integer, String, Numeric, DateTime, Index
from datetime import datetime
from app.database import Base
class MarketPrice(Base):
"""
Cache table for market prices from Yahoo Finance.
Stores the last fetched price for each symbol to reduce API calls.
"""
__tablename__ = "market_prices"
id = Column(Integer, primary_key=True, index=True)
symbol = Column(String(20), unique=True, nullable=False, index=True)
price = Column(Numeric(precision=20, scale=6), nullable=False)
fetched_at = Column(DateTime, nullable=False, default=datetime.utcnow)
source = Column(String(50), default="yahoo_finance")
# Index for quick lookups by symbol and freshness checks
__table_args__ = (
Index('idx_symbol_fetched', 'symbol', 'fetched_at'),
)
def __repr__(self):
return f"<MarketPrice(symbol={self.symbol}, price={self.price}, fetched_at={self.fetched_at})>"

View File

@@ -0,0 +1,104 @@
"""Position model representing a trading position."""
from sqlalchemy import Column, Integer, String, DateTime, Numeric, ForeignKey, Date, Enum, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.database import Base
class PositionType(str, enum.Enum):
"""Enumeration of position types."""
STOCK = "stock"
CALL = "call"
PUT = "put"
class PositionStatus(str, enum.Enum):
"""Enumeration of position statuses."""
OPEN = "open"
CLOSED = "closed"
class Position(Base):
"""
Represents a trading position (open or closed).
A position aggregates related transactions (entries and exits) for a specific security.
For options, tracks strikes, expirations, and option-specific details.
Attributes:
id: Primary key
account_id: Foreign key to account
symbol: Base trading symbol (e.g., AAPL)
option_symbol: Full option symbol if applicable (e.g., -AAPL260116C150)
position_type: Type (stock, call, put)
status: Status (open, closed)
open_date: Date position was opened
close_date: Date position was closed (if closed)
total_quantity: Net quantity (can be negative for short positions)
avg_entry_price: Average entry price
avg_exit_price: Average exit price (if closed)
realized_pnl: Realized profit/loss for closed positions
unrealized_pnl: Unrealized profit/loss for open positions
created_at: Timestamp of record creation
updated_at: Timestamp of last update
"""
__tablename__ = "positions"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
# Symbol information
symbol = Column(String(50), nullable=False, index=True)
option_symbol = Column(String(100), index=True) # Full option symbol for options
position_type = Column(Enum(PositionType), nullable=False, default=PositionType.STOCK)
# Status and dates
status = Column(Enum(PositionStatus), nullable=False, default=PositionStatus.OPEN, index=True)
open_date = Column(Date, nullable=False)
close_date = Column(Date)
# Position metrics
total_quantity = Column(Numeric(20, 8), nullable=False) # Can be negative for short
avg_entry_price = Column(Numeric(20, 8))
avg_exit_price = Column(Numeric(20, 8))
# P&L tracking
realized_pnl = Column(Numeric(20, 2)) # For closed positions
unrealized_pnl = Column(Numeric(20, 2)) # For open positions
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now(), nullable=False)
# Relationships
account = relationship("Account", back_populates="positions")
transaction_links = relationship("PositionTransaction", back_populates="position", cascade="all, delete-orphan")
# Composite indexes for common queries
__table_args__ = (
Index('idx_account_status', 'account_id', 'status'),
Index('idx_account_symbol_status', 'account_id', 'symbol', 'status'),
)
class PositionTransaction(Base):
"""
Junction table linking positions to transactions.
A position can have multiple transactions (entries, exits, adjustments).
A transaction can be part of multiple positions (e.g., closing multiple lots).
Attributes:
position_id: Foreign key to position
transaction_id: Foreign key to transaction
"""
__tablename__ = "position_transactions"
position_id = Column(Integer, ForeignKey("positions.id", ondelete="CASCADE"), primary_key=True)
transaction_id = Column(Integer, ForeignKey("transactions.id", ondelete="CASCADE"), primary_key=True)
# Relationships
position = relationship("Position", back_populates="transaction_links")
transaction = relationship("Transaction", back_populates="position_links")

View File

@@ -0,0 +1,81 @@
"""Transaction model representing a brokerage transaction."""
from sqlalchemy import Column, Integer, String, DateTime, Numeric, ForeignKey, Date, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Transaction(Base):
"""
Represents a single brokerage transaction.
Attributes:
id: Primary key
account_id: Foreign key to account
run_date: Date the transaction was recorded
action: Description of the transaction action
symbol: Trading symbol
description: Full transaction description
transaction_type: Type (Cash/Margin)
exchange_quantity: Quantity in exchange currency
exchange_currency: Exchange currency code
currency: Transaction currency
price: Transaction price per unit
quantity: Number of shares/contracts
exchange_rate: Currency exchange rate
commission: Commission fees
fees: Additional fees
accrued_interest: Interest accrued
amount: Total transaction amount
cash_balance: Account balance after transaction
settlement_date: Date transaction settles
unique_hash: SHA-256 hash for deduplication
created_at: Timestamp of record creation
updated_at: Timestamp of last update
"""
__tablename__ = "transactions"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
# Transaction details from CSV
run_date = Column(Date, nullable=False, index=True)
action = Column(String(500), nullable=False)
symbol = Column(String(50), index=True)
description = Column(String(500))
transaction_type = Column(String(20)) # Cash, Margin
# Quantities and currencies
exchange_quantity = Column(Numeric(20, 8))
exchange_currency = Column(String(10))
currency = Column(String(10))
# Financial details
price = Column(Numeric(20, 8))
quantity = Column(Numeric(20, 8))
exchange_rate = Column(Numeric(20, 8))
commission = Column(Numeric(20, 2))
fees = Column(Numeric(20, 2))
accrued_interest = Column(Numeric(20, 2))
amount = Column(Numeric(20, 2))
cash_balance = Column(Numeric(20, 2))
settlement_date = Column(Date)
# Deduplication hash
unique_hash = Column(String(64), unique=True, nullable=False, index=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now(), nullable=False)
# Relationships
account = relationship("Account", back_populates="transactions")
position_links = relationship("PositionTransaction", back_populates="transaction", cascade="all, delete-orphan")
# Composite index for common queries
__table_args__ = (
Index('idx_account_date', 'account_id', 'run_date'),
Index('idx_account_symbol', 'account_id', 'symbol'),
)