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,257 @@
"""Fidelity brokerage CSV parser."""
from pathlib import Path
from typing import List, Dict, Any
import pandas as pd
from datetime import datetime
import re
from app.parsers.base_parser import BaseParser, ParseResult
class FidelityParser(BaseParser):
"""
Parser for Fidelity brokerage account history CSV files.
Expected CSV columns:
- Run Date
- Action
- Symbol
- Description
- Type
- Exchange Quantity
- Exchange Currency
- Currency
- Price
- Quantity
- Exchange Rate
- Commission
- Fees
- Accrued Interest
- Amount
- Cash Balance
- Settlement Date
"""
# Expected column names in Fidelity CSV
EXPECTED_COLUMNS = [
"Run Date",
"Action",
"Symbol",
"Description",
"Type",
"Exchange Quantity",
"Exchange Currency",
"Currency",
"Price",
"Quantity",
"Exchange Rate",
"Commission",
"Fees",
"Accrued Interest",
"Amount",
"Cash Balance",
"Settlement Date",
]
def parse(self, file_path: Path) -> ParseResult:
"""
Parse a Fidelity CSV file into standardized transaction dictionaries.
Args:
file_path: Path to the Fidelity CSV file
Returns:
ParseResult containing parsed transactions, errors, and row count
Raises:
FileNotFoundError: If the file does not exist
ValueError: If the CSV format is invalid
"""
errors = []
transactions = []
try:
# Read CSV, skipping empty rows at the beginning
df = self._read_csv(file_path, skiprows=self._find_header_row(file_path))
# Validate columns
missing_cols = set(self.EXPECTED_COLUMNS) - set(df.columns)
if missing_cols:
raise ValueError(f"Missing required columns: {missing_cols}")
# Parse each row
for idx, row in df.iterrows():
try:
transaction = self._parse_row(row)
if transaction:
transactions.append(transaction)
except Exception as e:
errors.append(f"Row {idx + 1}: {str(e)}")
return ParseResult(
transactions=transactions, errors=errors, row_count=len(df)
)
except FileNotFoundError as e:
raise e
except Exception as e:
raise ValueError(f"Failed to parse Fidelity CSV: {str(e)}")
def _find_header_row(self, file_path: Path) -> int:
"""
Find the row number where the header starts in Fidelity CSV.
Fidelity CSVs may have empty rows or metadata at the beginning.
Args:
file_path: Path to CSV file
Returns:
Row number (0-indexed) where the header is located
"""
with open(file_path, "r", encoding="utf-8-sig") as f:
for i, line in enumerate(f):
if "Run Date" in line:
return i
return 0 # Default to first row if not found
def _extract_real_ticker(self, symbol: str, description: str, action: str) -> str:
"""
Extract the real underlying ticker from option descriptions.
Fidelity uses internal reference numbers (like 6736999MM) in the Symbol column
for options, but the real ticker is in the Description/Action in parentheses.
Examples:
- Description: "CALL (OPEN) OPENDOOR JAN 16 26 (100 SHS)"
- Action: "YOU SOLD CLOSING TRANSACTION CALL (OPEN) OPENDOOR..."
Args:
symbol: Symbol from CSV (might be Fidelity internal reference)
description: Description field
action: Action field
Returns:
Real ticker symbol, or original symbol if not found
"""
# If symbol looks normal (letters only, not Fidelity's numeric codes), return it
if symbol and re.match(r'^[A-Z]{1,5}$', symbol):
return symbol
# Try to extract from description first (more reliable)
# Pattern: (TICKER) or CALL (TICKER) or PUT (TICKER)
if description:
# Look for pattern like "CALL (OPEN)" or "PUT (AAPL)"
match = re.search(r'(?:CALL|PUT)\s*\(([A-Z]+)\)', description, re.IGNORECASE)
if match:
return match.group(1)
# Look for standalone (TICKER) pattern
match = re.search(r'\(([A-Z]{1,5})\)', description)
if match:
ticker = match.group(1)
# Make sure it's not something like (100 or (Margin)
if not ticker.isdigit() and ticker not in ['MARGIN', 'CASH', 'SHS']:
return ticker
# Fall back to action field
if action:
match = re.search(r'(?:CALL|PUT)\s*\(([A-Z]+)\)', action, re.IGNORECASE)
if match:
return match.group(1)
# Return original symbol if we couldn't extract anything better
return symbol if symbol else None
def _parse_row(self, row: pd.Series) -> Dict[str, Any]:
"""
Parse a single row from Fidelity CSV into a transaction dictionary.
Args:
row: Pandas Series representing one CSV row
Returns:
Dictionary with transaction data, or None if row should be skipped
Raises:
ValueError: If required fields are missing or invalid
"""
# Parse dates
run_date = self._parse_date(row["Run Date"])
settlement_date = self._parse_date(row["Settlement Date"])
# Extract raw values
raw_symbol = self._safe_string(row["Symbol"])
description = self._safe_string(row["Description"])
action = str(row["Action"]).strip() if pd.notna(row["Action"]) else ""
# Extract the real ticker (especially important for options)
actual_symbol = self._extract_real_ticker(raw_symbol, description, action)
# Extract and clean values
transaction = {
"run_date": run_date,
"action": action,
"symbol": actual_symbol,
"description": description,
"transaction_type": self._safe_string(row["Type"]),
"exchange_quantity": self._safe_decimal(row["Exchange Quantity"]),
"exchange_currency": self._safe_string(row["Exchange Currency"]),
"currency": self._safe_string(row["Currency"]),
"price": self._safe_decimal(row["Price"]),
"quantity": self._safe_decimal(row["Quantity"]),
"exchange_rate": self._safe_decimal(row["Exchange Rate"]),
"commission": self._safe_decimal(row["Commission"]),
"fees": self._safe_decimal(row["Fees"]),
"accrued_interest": self._safe_decimal(row["Accrued Interest"]),
"amount": self._safe_decimal(row["Amount"]),
"cash_balance": self._safe_decimal(row["Cash Balance"]),
"settlement_date": settlement_date,
}
return transaction
def _parse_date(self, date_value: Any) -> Any:
"""
Parse date value from CSV, handling various formats.
Args:
date_value: Date value from CSV (string or datetime)
Returns:
datetime.date object or None if empty/invalid
"""
if pd.isna(date_value) or date_value == "":
return None
# If already a datetime object
if isinstance(date_value, datetime):
return date_value.date()
# Try parsing common date formats
date_str = str(date_value).strip()
if not date_str:
return None
# Try common formats
for fmt in ["%m/%d/%Y", "%Y-%m-%d", "%m-%d-%Y"]:
try:
return datetime.strptime(date_str, fmt).date()
except ValueError:
continue
return None
def _safe_string(self, value: Any) -> str:
"""
Safely convert value to string, handling NaN and empty values.
Args:
value: Value to convert
Returns:
String value or None if empty
"""
if pd.isna(value) or value == "":
return None
return str(value).strip()