Nate's Project Guide

Project: Portfolio Tracker Category: Web App (Flask) + Data Science Last updated: April 21

Note: This guide reflects the latest state of your project repo. It may not match the most up-to-date version if you've worked since.

Where You Are

You're on the right track and in a good place with main.py.

This week we're turning it into a Flask web app.

Two things we need to clean up in passing:

  • Logic and printing are tangled. display_industry_allocation computes allocation and prints it in the same loop. For the web, we need functions that return data (so a template can render it). We'll pull that apart.
  • No pyproject.toml yet. uv hasn't been initialized. We'll do that when we add Flask.
  • Minor: test.py is a copy of main.py — it can go. And data/portfolio_analysis.py is leftover from Checkpoint 1, delete it too.

Project Structure

Your project splits into two kinds of code:

  • Business logic — you handwrite this. Everything in portfolio.py (NEW): loading the CSV, computing total value, computing gain/loss per stock, computing allocation by sector, the concentration warning rule. These ARE your tracker. No print(), no flask imports — every function returns data.
  • View / Flask code — agent-assisted is fine. app.py is thin (one route that calls the functions in portfolio.py and hands the results to a template). templates/index.html is HTML + Tailwind utility classes. None of it decides anything financial.

Target layout by Thursday:

final-project-NateMalkin/
├── app.py                  ← Flask route — agent-assisted OK
├── portfolio.py            ← business logic — handwrite (yours to own) NEW
├── main.py                 ← CLI (optional — keep if you want both)
├── pyproject.toml          ← created by uv init
├── templates/
│   └── index.html          ← Tailwind dashboard — agent-assisted OK
└── data/
    └── portfolio.csv

portfolio.py should not print anything. It returns dicts, lists, and numbers. app.py and the template handle the display. If you ever add a print() to portfolio.py, you've leaked view code into the business logic.

Phase 1: Extract portfolio.py

Objective

Create portfolio.py at the project root. Move your existing math functions in. Add four new portfolio-wide functions that return data instead of printing.

Instructions

Example Structure

def total_value(portfolio):
    """Sum of shares * current_price for every stock."""
    # one line with sum(...) and a generator expression should do it
    ...


def gain_loss(portfolio):
    """Return [{'ticker', 'gain', 'percent'}, ...] — one entry per stock."""
    # reuse calculate_gain_loss(stock) here — don't recompute the math
    ...


def allocation(portfolio):
    """Return {industry: percent_of_total_value, ...}.

    Percent is based on MARKET VALUE (shares * current_price),
    not share count. "% of portfolio" means dollars.
    """
    # step 1: total = total_value(portfolio)
    # step 2: walk the portfolio, accumulate dollars per industry in a dict
    #         (use dict.get(key, 0) so the first stock in a new industry works)
    # step 3: divide each industry total by `total` and multiply by 100
    ...


def check_concentration(alloc, threshold=40.0):
    """Return a list of warning strings for any industry above threshold."""
    # empty list if nothing is over the threshold
    ...

Why "returns data, not prints"? Your CLI printed the report. The browser can't read print() — it reads whatever render_template gets handed. If allocation() returns a dict, you can loop it in a Jinja template and pprint it in a terminal and unit-test it. If it prints, you can only watch it scroll by.

Optional — get help from your agent:

Walk me through the difference between my old display_industry_allocation
in main.py (which prints percentages from share counts) and the new
allocation(portfolio) in portfolio.py (which returns a dict of percent
by market value). Why does the market-value version matter?
Don't change my code — just explain.

Phase 2: Initialize uv and add Flask

Agent-assisted is fine here. Every Flask project starts the same way.

Objective

Get pyproject.toml on disk, install Flask, and confirm a "hello" page loads in the browser.

Instructions

Hints

Minimal app.py (replace with the real one in Phase 3):

from flask import Flask

app = Flask(__name__)


@app.route("/")
def home():
    return "hello"

--debug makes Flask reload when you save a file, so you don't have to restart the server every time.

Optional — get help from your agent:

Skip — uv init, uv add flask, and hello-world are two commands and six lines.

Phase 3: Wire up the / route

Agent-assisted OK here. The route is thin — it calls your portfolio.py functions and hands the results to the template. You already wrote the hard part.

Objective

Replace the "hello" route with one that loads the portfolio, calls all four portfolio.py functions, and renders index.html.

Instructions

Hints

Full app.py:

from flask import Flask, render_template

# import your functions from your business logic module
from portfolio import (
    allocation,
    check_concentration,
    gain_loss,
    load_portfolio,
    total_value,
)

app = Flask(__name__)


@app.route("/")
def home():
    portfolio = load_portfolio("data/portfolio.csv")
    alloc = allocation(portfolio)
    return render_template(
        "index.html",
        portfolio=portfolio,
        total=total_value(portfolio),
        gains=gain_loss(portfolio),
        alloc=alloc,
        warnings=check_concentration(alloc),
    )

Optional — get help from your agent:

Here's my app.py. Walk me through what render_template does with the
keyword arguments I pass it. How do they show up in the template?
Don't change my code.

Phase 4: Build the dashboard template with Tailwind

Agent-assisted OK here. HTML and utility classes are library code. Read what's generated, tweak the copy, move on.

Objective

A single templates/index.html that renders: the total value up top, a concentration warning if any sector is over-weighted, a holdings table, a gain/loss list with green/red, and an allocation bar chart.

Instructions

Hints

Full templates/index.html (copy/paste is fine):

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Portfolio Tracker</title>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="min-h-screen bg-slate-950 text-slate-100 antialiased">
    <div class="mx-auto max-w-5xl px-6 py-12">
      <header class="mb-10">
        <p class="text-sm uppercase tracking-widest text-slate-400">
          Portfolio Tracker
        </p>
        <h1 class="mt-2 text-4xl font-bold tracking-tight text-white">
          ${{ "{:,.2f}".format(total) }}
        </h1>
        <p class="mt-1 text-sm text-slate-400">
          Total market value across {{ portfolio|length }} holdings
        </p>
      </header>

      {% if warnings %}
      <section
        class="mb-8 rounded-2xl border border-amber-500/40 bg-amber-500/10 p-5"
      >
        <h2
          class="text-sm font-semibold uppercase tracking-wide text-amber-300"
        >
          Concentration warnings
        </h2>
        <ul class="mt-2 space-y-1 text-sm text-amber-100">
          {% for w in warnings %}
          <li>{{ w }}</li>
          {% endfor %}
        </ul>
      </section>
      {% endif %}

      <div class="grid gap-6 md:grid-cols-2">
        <section
          class="rounded-2xl border border-slate-800 bg-slate-900/60 p-6 shadow-xl shadow-slate-950/50"
        >
          <h2 class="mb-4 text-lg font-semibold text-white">Holdings</h2>
          <table class="w-full text-left text-sm">
            <thead class="text-xs uppercase text-slate-400">
              <tr>
                <th class="pb-2">Ticker</th>
                <th class="pb-2">Shares</th>
                <th class="pb-2">Avg Cost</th>
                <th class="pb-2">Price</th>
                <th class="pb-2">Industry</th>
              </tr>
            </thead>
            <tbody class="divide-y divide-slate-800">
              {% for stock in portfolio %}
              <tr>
                <td class="py-2 font-semibold text-white">
                  {{ stock.ticker }}
                </td>
                <td class="py-2">{{ stock.shares }}</td>
                <td class="py-2">${{ "{:,.2f}".format(stock.avg_cost) }}</td>
                <td class="py-2">
                  ${{ "{:,.2f}".format(stock.current_price) }}
                </td>
                <td class="py-2 text-slate-400">{{ stock.industry }}</td>
              </tr>
              {% endfor %}
            </tbody>
          </table>
        </section>

        <section
          class="rounded-2xl border border-slate-800 bg-slate-900/60 p-6 shadow-xl shadow-slate-950/50"
        >
          <h2 class="mb-4 text-lg font-semibold text-white">Gain / Loss</h2>
          <ul class="divide-y divide-slate-800">
            {% for row in gains %}
            <li class="flex items-center justify-between py-3">
              <span class="font-semibold text-white">{{ row.ticker }}</span>
              <span
                class="text-sm font-medium {{ 'text-emerald-400' if row.gain >= 0 else 'text-rose-400' }}"
              >
                {{ '+' if row.gain >= 0 else '' }}${{ "{:,.2f}".format(row.gain)
                }}
                <span class="ml-2 text-xs text-slate-400">
                  ({{ '+' if row.percent >= 0 else '' }}{{
                  "%.1f"|format(row.percent) }}%)
                </span>
              </span>
            </li>
            {% endfor %}
          </ul>
        </section>
      </div>

      <section
        class="mt-6 rounded-2xl border border-slate-800 bg-slate-900/60 p-6 shadow-xl shadow-slate-950/50"
      >
        <h2 class="mb-4 text-lg font-semibold text-white">
          Allocation by industry
        </h2>
        <ul class="space-y-3">
          {% for industry, pct in alloc.items() %}
          <li>
            <div class="mb-1 flex items-center justify-between text-sm">
              <span class="text-slate-200">{{ industry }}</span>
              <span class="text-slate-400">{{ "%.1f"|format(pct) }}%</span>
            </div>
            <div class="h-2 w-full overflow-hidden rounded-full bg-slate-800">
              <div
                class="h-full rounded-full bg-violet-500"
                style="width: {{ pct }}%"
              ></div>
            </div>
          </li>
          {% endfor %}
        </ul>
      </section>

      <footer class="mt-10 text-center text-xs text-slate-500">
        Data from
        <code>data/portfolio.csv</code>
      </footer>
    </div>
  </body>
</html>

Optional — get help from your agent (after your route works with real data):

Here's my index.html rendering my portfolio. Keep the Jinja loops
and the data keys exactly as they are. Help me polish:
- make the header total value change color (emerald if the portfolio
  is up overall, rose if it's down — I'll pass a boolean from app.py)
- add a subtle hover state on the holdings table rows
- make sure it looks decent on mobile (narrow viewport)
Don't change app.py or portfolio.py.

Phase 5: Cleanup, README, and journal

Handwrite the journal. This is what I use to write next week's guide, and what you'll say on demo day.

Objective

Delete the Checkpoint 1 leftovers, update the README so someone else can run your app, and fill in the Checkpoint 2 entry.

Instructions

    • What's in portfolio.py (handwritten — the four aggregate functions)
    • What's in app.py and templates/index.html (agent-assisted — Flask route + Tailwind template)
    • What changed vs. Checkpoint 1 (logic split out of display_*, allocation now by dollars, app runs in the browser)
    • Anything still rough

Hints

Commit message idea:

checkpoint 2: portfolio.py + flask dashboard with tailwind

Phase 6 (preview — not required for Checkpoint 2): put CAPM and best/worst on the dashboard

You already have calculate_capm(beta) in your code. Two small adds turn that into dashboard rows:

  • expected_returns(portfolio) in portfolio.py — returns [{"ticker", "capm_pct"}, ...] by walking the portfolio and calling calculate_capm(stock["beta"])
  • best_and_worst(portfolio) — returns {"best": {...}, "worst": {...}} using the same gain/loss math, picked by max(...) and min(...) on percent

Add two more template sections that render them. Same pattern as the Gain/Loss list — the template loops a list of dicts, Python decides what's in each dict.

Don't chase this until Phases 1–5 are solid.

Checkpoint 2 Readiness

By Thursday April 23 at 3pm:

Helpful Resources