Skip to content

Portfolio API

The Portfolio API provides comprehensive portfolio management functionality, allowing users to track stock holdings, analyze performance, and manage investments.

Base URL

All portfolio endpoints are prefixed with:

/api/v1/portfolio/

Authentication

All portfolio endpoints require authentication. Include the Bearer token in the Authorization header:

Authorization: Bearer <your_jwt_token>

Data Models

Portfolio Response

{
  "stocks": [
    {
      "symbol": "AAPL",
      "name": "Apple Inc.",
      "shares": 10.5,
      "averagePrice": 150.25,
      "currentPrice": 175.80,
      "totalValue": 1845.90,
      "gain": 268.28,
      "gainPercent": 17.02
    }
  ],
  "totalValue": 25430.50,
  "dailyChange": 1250.75,
  "dailyChangePercent": 5.17
}

Portfolio Stock

Field Type Description
symbol string Stock ticker symbol (e.g., "AAPL")
name string Company name
shares number Number of shares owned
averagePrice number Average purchase price per share
currentPrice number Current market price per share
totalValue number Current total value (shares × currentPrice)
gain number Total gain/loss in dollars
gainPercent number Total gain/loss as percentage

Endpoints

Get Portfolio

Retrieve the user's complete portfolio with real-time market data.

GET /api/v1/portfolio/

Response

Status Code: 200 OK

{
  "stocks": [
    {
      "symbol": "AAPL",
      "name": "Apple Inc.",
      "shares": 10,
      "averagePrice": 150.00,
      "currentPrice": 175.50,
      "totalValue": 1755.00,
      "gain": 255.00,
      "gainPercent": 17.00
    },
    {
      "symbol": "GOOGL",
      "name": "Alphabet Inc.",
      "shares": 5,
      "averagePrice": 2800.00,
      "currentPrice": 2950.25,
      "totalValue": 14751.25,
      "gain": 751.25,
      "gainPercent": 5.37
    }
  ],
  "totalValue": 16506.25,
  "dailyChange": 425.75,
  "dailyChangePercent": 2.65
}

Error Responses

404 Not Found

{
  "detail": "Portfolio not found"
}

500 Internal Server Error

{
  "detail": "Failed to retrieve portfolio"
}


Add Stock to Portfolio

Add a new stock to the portfolio or increase an existing position.

POST /api/v1/portfolio/stocks

Request Body

{
  "symbol": "AAPL",
  "shares": 10.5,
  "price": 150.25
}
Field Type Required Description
symbol string Yes Stock ticker symbol (e.g., "AAPL")
shares number Yes Number of shares to add (> 0)
price number Yes Purchase price per share (> 0)

Response

Status Code: 200 OK

{
  "message": "Successfully added 10.5 shares of AAPL to portfolio"
}

Error Responses

400 Bad Request - Invalid stock symbol

{
  "detail": "Invalid stock symbol: INVALID"
}

422 Unprocessable Entity - Validation errors

{
  "detail": [
    {
      "loc": ["body", "shares"],
      "msg": "ensure this value is greater than 0",
      "type": "value_error.number.not_gt",
      "ctx": {"limit_value": 0}
    }
  ]
}

500 Internal Server Error

{
  "detail": "Failed to add stock to portfolio"
}


Update Stock in Portfolio

Update the number of shares or average price for an existing holding.

PUT /api/v1/portfolio/stocks/{symbol}

Path Parameters

Parameter Type Description
symbol string Stock ticker symbol to update

Request Body

{
  "shares": 15.0,
  "average_price": 145.50
}
Field Type Required Description
shares number No New total number of shares (> 0)
average_price number No New average price per share (> 0)

Note: At least one field must be provided.

Response

Status Code: 200 OK

{
  "message": "Successfully updated AAPL in portfolio"
}

Error Responses

404 Not Found

{
  "detail": "Stock AAPL not found in portfolio"
}

422 Unprocessable Entity

{
  "detail": [
    {
      "loc": ["body", "shares"],
      "msg": "ensure this value is greater than 0",
      "type": "value_error.number.not_gt",
      "ctx": {"limit_value": 0}
    }
  ]
}

500 Internal Server Error

{
  "detail": "Failed to update AAPL in portfolio"
}


Remove Stock from Portfolio

Remove a specified number of shares from an existing holding.

DELETE /api/v1/portfolio/stocks/{symbol}

Path Parameters

Parameter Type Description
symbol string Stock ticker symbol to remove shares from

Request Body

{
  "shares": 5.0
}
Field Type Required Description
shares number Yes Number of shares to remove (> 0)

Note: If the number of shares to remove equals or exceeds the current holding, the entire position will be removed from the portfolio.

Response

Status Code: 200 OK

{
  "message": "Successfully removed 5.0 shares of AAPL from portfolio"
}

Error Responses

404 Not Found

{
  "detail": "Stock AAPL not found in portfolio"
}

422 Unprocessable Entity

{
  "detail": [
    {
      "loc": ["body", "shares"],
      "msg": "ensure this value is greater than 0",
      "type": "value_error.number.not_gt",
      "ctx": {"limit_value": 0}
    }
  ]
}

500 Internal Server Error

{
  "detail": "Failed to remove AAPL from portfolio"
}

Examples

Complete Portfolio Management Workflow

1. Get Current Portfolio

curl -X GET "http://localhost:8000/api/v1/portfolio/" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."

2. Add New Stock

curl -X POST "http://localhost:8000/api/v1/portfolio/stocks" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "AAPL",
    "shares": 10,
    "price": 150.00
  }'

3. Update Existing Position

curl -X PUT "http://localhost:8000/api/v1/portfolio/stocks/AAPL" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "shares": 15
  }'

4. Partial Sale

curl -X DELETE "http://localhost:8000/api/v1/portfolio/stocks/AAPL" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "shares": 5
  }'

JavaScript/TypeScript Examples

Using Fetch API

// Get portfolio
async function getPortfolio() {
  const response = await fetch('/api/v1/portfolio/', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`Error ${response.status}: ${response.statusText}`);
  }

  return await response.json();
}

// Add stock to portfolio
async function addStock(symbol, shares, price) {
  const response = await fetch('/api/v1/portfolio/stocks', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ symbol, shares, price })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.detail);
  }

  return await response.json();
}

Using Axios

import axios from 'axios';

const api = axios.create({
  baseURL: '/api/v1',
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

// Get portfolio
const portfolio = await api.get('/portfolio/');

// Add stock
const result = await api.post('/portfolio/stocks', {
  symbol: 'AAPL',
  shares: 10,
  price: 150.00
});

// Update stock
await api.put('/portfolio/stocks/AAPL', {
  shares: 15
});

// Remove shares
await api.delete('/portfolio/stocks/AAPL', {
  data: { shares: 5 }
});

Python Examples

import httpx
import asyncio

async def get_portfolio(token: str):
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "http://localhost:8000/api/v1/portfolio/",
            headers={"Authorization": f"Bearer {token}"}
        )
        response.raise_for_status()
        return response.json()

async def add_stock(token: str, symbol: str, shares: float, price: float):
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "http://localhost:8000/api/v1/portfolio/stocks",
            headers={"Authorization": f"Bearer {token}"},
            json={"symbol": symbol, "shares": shares, "price": price}
        )
        response.raise_for_status()
        return response.json()

# Usage
portfolio = await get_portfolio(token)
result = await add_stock(token, "AAPL", 10, 150.00)

Business Logic

Portfolio Calculations

Average Price Calculation

When adding shares to an existing position, the average price is calculated using a weighted average:

new_average_price = (existing_shares * existing_avg_price + new_shares * new_price) / (existing_shares + new_shares)

Gain/Loss Calculation

  • Gain/Loss ($): (current_price - average_price) * shares
  • Gain/Loss (%): ((current_price - average_price) / average_price) * 100

Portfolio Totals

  • Total Value: Sum of all individual stock values
  • Daily Change: Sum of all individual stock daily changes
  • Daily Change %: (daily_change / (total_value - daily_change)) * 100

Stock Symbol Validation

Before adding a stock to the portfolio, the system validates that the symbol exists by: 1. Checking with the market data service 2. Ensuring the symbol returns valid price data 3. Rejecting invalid or non-existent symbols

Error Handling

The API implements comprehensive error handling: - Validation Errors: Invalid input data (negative shares, invalid symbols) - Business Logic Errors: Stock not found in portfolio - System Errors: Database connection issues, external API failures - Authentication Errors: Invalid or expired tokens

Performance Considerations

  • Portfolio data includes real-time stock prices
  • Market data is cached for 60 seconds to reduce API calls
  • Database queries are optimized with proper indexing
  • Bulk operations are supported for better performance

Rate Limiting

Portfolio endpoints are subject to rate limiting: - Read Operations: 100 requests per minute - Write Operations: 50 requests per minute

Testing

Unit Tests

Portfolio endpoints are covered by comprehensive unit tests:

# Run portfolio-specific tests
uv run pytest tests/unit/test_services/test_portfolio_service.py -v

# Run portfolio API tests
uv run pytest tests/integration/test_api/test_portfolio.py -v

Test Data

Test portfolios can be created using factories:

from tests.factories import UserFactory, PortfolioFactory, HoldingFactory

user = UserFactory.create()
portfolio = PortfolioFactory.create(user=user)
holding = HoldingFactory.create(portfolio=portfolio, symbol="AAPL")

For more information about testing, see the Testing Guide.