CodeForFinance

Track Your Portfolio in Python

Every broker has a portfolio page. Most of them are mediocre. Building your own tracker takes an afternoon, fits your exact needs, and teaches you more about Python and finance than a dozen tutorials. The script below fetches live prices, computes P&L, and emails you a daily summary.

Install Dependencies

pip install yfinance pandas tabulate python-dotenv

yfinance scrapes Yahoo Finance, which is free but unofficial. For something more robust use Alpha Vantage, Tiingo, or a paid data vendor. For personal tracking, yfinance is fine.

Define Your Holdings

Start with your holdings as a list of dicts. In production you would store this in SQLite or Postgres, but for a personal tracker an in-file list works. Each holding needs a ticker, quantity, and cost basis.

import pandas as pd
import yfinance as yf
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
import os
from dotenv import load_dotenv

load_dotenv()

# Your portfolio - could be a CSV, database, whatever
HOLDINGS = [
    {'ticker': 'AAPL',   'shares': 10, 'cost_basis': 150.00},
    {'ticker': 'MSFT',   'shares': 5,  'cost_basis': 300.00},
    {'ticker': 'VWRL.L', 'shares': 50, 'cost_basis': 95.50},
    {'ticker': 'GOOGL',  'shares': 8,  'cost_basis': 120.00},
]

Fetch Prices and Compute P&L

We pull two days of closes so we can compute the day change. For each holding we calculate market value, total P&L against cost basis, and the single-day move in both pounds and percent. Note that VWRL.L is the London Stock Exchange ticker suffix - yfinance needs exchange suffixes for non-US tickers.

def fetch_prices(tickers):
    data = yf.download(tickers, period='2d', progress=False)
    # Latest close and previous close
    closes = data['Close'].iloc[-1]
    prev_closes = data['Close'].iloc[-2]
    return closes, prev_closes

def build_portfolio_df():
    df = pd.DataFrame(HOLDINGS)
    tickers = df['ticker'].tolist()
    closes, prev_closes = fetch_prices(tickers)

    df['price'] = df['ticker'].map(closes)
    df['prev_price'] = df['ticker'].map(prev_closes)

    df['market_value'] = df['shares'] * df['price']
    df['cost_value']   = df['shares'] * df['cost_basis']
    df['total_pnl']    = df['market_value'] - df['cost_value']
    df['total_pnl_pct'] = df['total_pnl'] / df['cost_value'] * 100
    df['day_pnl']      = df['shares'] * (df['price'] - df['prev_price'])
    df['day_pnl_pct']  = (df['price'] / df['prev_price'] - 1) * 100
    return df

Build the Report

A plain text report is more than enough for daily emails. Readable on any device, no rendering issues, no spam filter problems. If you want colours and charts later, upgrade to HTML and matplotlib, but start simple.

def build_report(df):
    total_mv = df['market_value'].sum()
    total_cost = df['cost_value'].sum()
    total_pnl = total_mv - total_cost
    total_pnl_pct = total_pnl / total_cost * 100
    day_pnl = df['day_pnl'].sum()
    day_pct = day_pnl / (total_mv - day_pnl) * 100

    header = f"Portfolio Report - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
    summary = (
        f"Total value: GBP {total_mv:,.2f}
"
        f"Total cost:  GBP {total_cost:,.2f}
"
        f"Total P&L:   GBP {total_pnl:,.2f} ({total_pnl_pct:+.2f}%)
"
        f"Day P&L:     GBP {day_pnl:,.2f} ({day_pct:+.2f}%)
"
    )

    # Sort by day P&L percentage
    df_sorted = df.sort_values('day_pnl_pct', ascending=False)
    cols = ['ticker', 'shares', 'price', 'market_value', 'day_pnl', 'day_pnl_pct', 'total_pnl', 'total_pnl_pct']
    table = df_sorted[cols].to_string(
        index=False,
        float_format=lambda x: f"{x:,.2f}"
    )

    return f"{header}

{summary}
{table}"

Send the Email

Gmail SMTP works out of the box if you generate an App Password (requires 2FA enabled on the account). Never put your main password in a script. For better deliverability later, move to Postmark, SendGrid, or SES.

def send_email(body, subject='Daily Portfolio Report'):
    sender = os.getenv('EMAIL_USER')
    password = os.getenv('EMAIL_PASS')
    recipient = os.getenv('EMAIL_TO')

    msg = MIMEText(body, 'plain')
    msg['Subject'] = subject
    msg['From'] = sender
    msg['To'] = recipient

    with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
        server.login(sender, password)
        server.send_message(msg)

if __name__ == '__main__':
    df = build_portfolio_df()
    report = build_report(df)
    print(report)
    send_email(report)

Schedule It

Put it in cron on a Linux box or use Windows Task Scheduler. The markets close at 4pm NY / 9pm UK, so schedule an hour after London close for the daily snapshot.

# Run every weekday at 5pm UK time
0 17 * * 1-5 /usr/bin/python3 /home/you/portfolio_tracker.py

Where to Take This Next

  • Historical snapshots - write daily portfolio value to a CSV or SQLite table so you can chart performance over time.
  • Benchmark comparison - pull SPY or VWRL returns and compare your portfolio performance to the market.
  • Risk metrics - compute beta, volatility, Sharpe ratio once you have enough history.
  • Dividend tracking - yfinance provides dividend data, add it to the report.
  • Telegram alerts - skip email and push to Telegram for instant alerts on big moves.
  • Rebalancing suggestions - add target weights and show drift from target.

The core tracker is maybe 80 lines. Each feature above is another 20 to 50 lines. Over a few weekends you end up with a portfolio tool better than anything your broker provides - and you understand every line of it.

Risk Warning

Price data from yfinance is delayed and occasionally inaccurate. Do not use this tool for trade execution decisions. Past portfolio performance does not predict future results.

Developer Essentials

As an Amazon Associate we may earn from qualifying purchases.