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:
104
backend/app/api/endpoints/positions.py
Normal file
104
backend/app/api/endpoints/positions.py
Normal 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}
|
||||
Reference in New Issue
Block a user