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,104 @@
"""Position API endpoints."""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from sqlalchemy import and_
from typing import List, Optional
from app.api.deps import get_db
from app.models import Position
from app.models.position import PositionStatus
from app.schemas import PositionResponse
router = APIRouter()
@router.get("", response_model=List[PositionResponse])
def list_positions(
account_id: Optional[int] = None,
status_filter: Optional[PositionStatus] = Query(
default=None, alias="status", description="Filter by position status"
),
symbol: Optional[str] = None,
skip: int = 0,
limit: int = Query(default=100, le=500),
db: Session = Depends(get_db),
):
"""
List positions with optional filtering.
Args:
account_id: Filter by account ID
status_filter: Filter by status (open/closed)
symbol: Filter by symbol
skip: Number of records to skip (pagination)
limit: Maximum number of records to return
db: Database session
Returns:
List of positions
"""
query = db.query(Position)
# Apply filters
if account_id:
query = query.filter(Position.account_id == account_id)
if status_filter:
query = query.filter(Position.status == status_filter)
if symbol:
query = query.filter(Position.symbol == symbol)
# Order by most recent first
query = query.order_by(Position.open_date.desc(), Position.id.desc())
# Pagination
positions = query.offset(skip).limit(limit).all()
return positions
@router.get("/{position_id}", response_model=PositionResponse)
def get_position(position_id: int, db: Session = Depends(get_db)):
"""
Get position by ID.
Args:
position_id: Position ID
db: Database session
Returns:
Position details
Raises:
HTTPException: If position not found
"""
position = db.query(Position).filter(Position.id == position_id).first()
if not position:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Position {position_id} not found",
)
return position
@router.post("/{account_id}/rebuild")
def rebuild_positions(account_id: int, db: Session = Depends(get_db)):
"""
Rebuild all positions for an account from transactions.
Args:
account_id: Account ID
db: Database session
Returns:
Number of positions created
"""
from app.services.position_tracker import PositionTracker
position_tracker = PositionTracker(db)
positions_created = position_tracker.rebuild_positions(account_id)
return {"positions_created": positions_created}