> Source URL: /unit-3/project-paths/nate-m/nate-m-2026-04-21.guide
# 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

- [ ] Create `portfolio.py` at the project root
- [ ] Move `load_portfolio`, `calculate_gain_loss`, `calculate_market_value`, and `calculate_capm` from `main.py` into `portfolio.py`
- [ ] Add `total_value(portfolio)` — sum of market value across every stock
- [ ] Add `gain_loss(portfolio)` — a **list** of dicts, one per stock: `{"ticker", "gain", "percent"}`
- [ ] Add `allocation(portfolio)` — a dict of `industry → percent of total portfolio value` (by dollars, not share count)
- [ ] Add `check_concentration(alloc, threshold=40.0)` — a **list** of warning strings, one per over-weighted sector
- [ ] Make sure `portfolio.py` does not `print` anything and does not `import flask`

### Example Structure

```python
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.

- [ ] **bug:** your current `display_industry_allocation` divides by **share count**. That makes 10 shares of a \$5 stock look the same as 10 shares of a \$500 stock. Do it by market value in the new `allocation()` — that's what "% of portfolio" actually means.

> **Optional — get help from your agent:**
>
> ```text
> 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

- [ ] From your project root, run `uv init`
- [ ] Run `uv add flask`
- [ ] Create `app.py` at the project root with the minimal Flask app below
- [ ] Run `uv run flask --app app run --debug` and open http://127.0.0.1:5000 — you should see "hello"

### Hints

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

```python
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

- [ ] Open `app.py`
- [ ] Import from `portfolio`: `load_portfolio`, `total_value`, `gain_loss`, `allocation`, `check_concentration`
- [ ] Inside `home()`, load the CSV once, then call the four aggregate functions
- [ ] Pass the results to `render_template("index.html", ...)`

### Hints

**Full `app.py`:**

```python
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:**
>
> ```text
> 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

- [ ] Create `templates/` at the project root
- [ ] Create `templates/index.html`
- [ ] Load Tailwind via the CDN `<script>` tag (see hint)
- [ ] Render each of the five sections using the data passed from `app.py`
- [ ] Refresh the browser — you should see your real portfolio with real numbers

### Hints

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

```html
<!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):**
>
> ```text
> 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

- [ ] Delete `test.py` (duplicate of `main.py`)
- [ ] Delete `data/portfolio_analysis.py` (Checkpoint 1 leftover, nothing imports it)
- [ ] In `main.py`, either delete the `display_*` functions (if you don't care about keeping a CLI) or leave them and have `main.py` import from `portfolio.py` so the two share the same math
- [ ] Update `README.md` — one paragraph of what it is, plus the `uv run flask --app app run` command
- [ ] Fill the **Checkpoint 2** section of `project.journal.md`:
  - 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
- [ ] Fill the **Checkpoint 1** section too if you haven't yet — a short catch-up paragraph
- [ ] Commit and push

### 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:

- [ ] `portfolio.py` exists and does **not** import `flask` or call `print`
- [ ] `portfolio.py` has `load_portfolio`, `calculate_gain_loss`, `calculate_market_value`, `calculate_capm`, `total_value`, `gain_loss`, `allocation`, `check_concentration`
- [ ] `pyproject.toml` exists with `flask` as a dependency
- [ ] `app.py` has one `/` route that loads the portfolio and calls the four aggregate functions
- [ ] `templates/index.html` renders total value, concentration warnings, holdings table, gain/loss list, and allocation bars — styled with Tailwind
- [ ] Concentration warning shows when any industry is > 40% of portfolio **by market value**
- [ ] `test.py` and `data/portfolio_analysis.py` deleted
- [ ] `project.spec.md` reflects the Flask pivot (update "Tech Stack" and the project description)
- [ ] Checkpoint 1 + Checkpoint 2 entries in `project.journal.md`
- [ ] `README.md` updated
- [ ] Committed and pushed

## Helpful Resources

- [Checkpoint 2 Instructions](../../projects/final-project-checkpoint-2.project.md)
- [Flask Setup Guide](../../resources/flask-setup.guide.md)
- [Lecture 1: The MVP](../../lectures/01-the-mvp/01-the-mvp.lecture.md)
- [Tailwind CSS docs (utility classes)](https://tailwindcss.com/docs/utility-first)
- [Jinja template syntax](https://jinja.palletsprojects.com/en/stable/templates/)


---

## Backlinks

The following sources link to this document:

- [April 21 -- Flask pivot + Tailwind dashboard](/unit-3/project-paths/nate-m/nate-m.path.llm.md)
