Day 1 · About 6 hours, in three sessions

Your first stock quotes

Set up Python, uv, Git and VS Code, and get free Alpaca keys for market data. Write a program that prints a stock’s latest quote, then a watchlist that refreshes every minute, and save both in Git.

  1. Session 1 Set up your machine, and your first quote
  2. Session 2 A watchlist of stocks and ETFs
  3. Session 3 Git: save your work, and go back to it

Today’s goal

By the end of today you will have a working setup and two programs. The first prints a stock’s latest quote:

uv run --env-file .env quote.py AAPLAAPL  last 330.27  bid 330.25  ask 330.30  spread 0.05

Shown for the course’s sample data. Yours shows the latest.

The second is a watchlist. Every minute it prints a table of shares and funds, until you press Ctrl+C:

uv run --env-file .env watchlist.py Quotes at 16:15:00symbol       last   change        %AAPL       330.27    +3.47   +1.06%MSFT       522.48    -2.62   -0.50%NVDA       185.12    +2.17   +1.19%SPY        771.53    +4.33   +0.56%QQQ        752.11    +3.21   +0.43% Stopped.

Shown for the course’s sample data. Yours shows the latest.

Both are saved in Git, so you can always go back to a version that worked, and your keys are kept out of it.

Why it matters on a desk

Every trading and risk system starts by reading prices. Before anyone can value what a firm owns, test a strategy or measure risk, a program has to fetch quotes, check them and pass them on.

Quant developers write that code. Traders watch the bid, the ask and the spread all day. Risk quants need prices to value every position a firm holds. Today’s two programs are the first piece of the trading desk you build in this course: its market data.

Session 1 Set up your machine, and your first quote

The idea

Step 1 Markets and exchanges

A share is a small piece of a company. A ticker is its short name: AAPL is Apple.An exchange runs a market: it takes buyers’ and sellers’ orders and matches them.Buyersbuy 100 at 330.25buy 50 at 330.20buy 200 at 330.10Sellerssell 80 at 330.30sell 300 at 330.35sell 40 at 330.40Exchangematches ordersNasdaq and the NYSE run exchangesEach order says how many shares, and at what price, in US dollars.
01/05

A share is a small piece of a company: owning one Apple share means owning a tiny part of Apple. Shares are bought and sold every working day, and their price moves as people trade them.

A ticker is the short name a share trades under. AAPL is Apple, MSFT is Microsoft and NVDA is Nvidia.

A market is where buyers and sellers meet. An exchange is a company that runs one: Nasdaq and the New York Stock Exchange (NYSE) are the best known in the United States. People send it orders, and it matches a buyer with a seller when their prices meet.

An order says what you want to do, how many shares, and at what price. “Buy 100 at 330.25” means you will buy 100 shares and pay at most 330.25 dollars for each. Prices on US exchanges are in US dollars.

Practice

Problem 1

2 points

A share’s bid is 52.40 and its ask is 52.46. What is the spread, in dollars?

Hint 1

The spread is the gap between the two prices.

Hint 2

Take the bid away from the ask.

Solution

spread = ask − bid

= 52.46 − 52.40

= 0.06 dollars

Problem 2

2 points

You buy 200 of those shares at the ask and sell them straight back at the bid. How many dollars do you lose?

Hint 1

You lose the spread on every share.

Hint 2

Multiply the spread by the 200 shares.

Solution

paid: 200 × 52.46 = 10,492.00

got back: 200 × 52.40 = 10,480.00

lost: 10,492.00 − 10,480.00 = 12.00

which is 200 × 0.06: shares × spread

You want to buy a share right now, at whatever the market asks. Which price do you pay?

Show the answer

B: The ask Sellers wait at the ask, the lowest price any of them will take, so to buy at once you meet them there. The last price is only what the latest trade paid.

Where should your Alpaca secret key be kept?

Show the answer

B: In .env, which .gitignore names Code and .env.example are committed, so anything in them ends up on GitHub. .env stays on your computer because .gitignore names it.

The project, step by step

Build it yourself from this brief, then check it against the steps.

  • Install uv, Git and VS Code, then make a project with uv init --no-package --python 3.14 --description "US equity market data in Python" market-data.
  • Add the httpx2 package with uv add httpx2.
  • Make a free Alpaca account and generate API keys for its paper trading account. Put them in .env as APCA_API_KEY_ID and APCA_API_SECRET_KEY, list the names in .env.example, and add .env to .gitignore.
  • Write quote.py. It takes a ticker from the command line, AAPL if there is none, asks https://data.alpaca.markets/v2/stocks/snapshots for its snapshot (the query symbols=AAPL&feed=delayed_sip, with your keys as the headers APCA-API-KEY-ID and APCA-API-SECRET-KEY), and prints one line: the last price, bid, ask and spread, each to two decimal places.
  • Check it with uv run --env-file .env quote.py AAPL and uv run --env-file .env quote.py MSFT.
  1. Step 1 Open a terminal

    A terminal is a window where you type commands and the computer answers in text. On Windows, open PowerShell from the Start menu. On macOS, open Terminal from Applications, then Utilities. On Linux, open your terminal app.

    Each command in this course comes after a prompt: $ on macOS and Linux, PS> on Windows. Type the command without the prompt, then press Enter.

  2. Step 2 Install uv

    uv is a tool that installs Python, gives each project its own copy of the packages it uses, and runs your code. Install it with the command for your computer:

    Windows

    powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

    macOS and Linux

    curl -LsSf https://astral.sh/uv/install.sh | sh

    On Linux it printed this; macOS and Windows print much the same:

    curl -LsSf https://astral.sh/uv/install.sh | shdownloading uv 0.12.23 x86_64-unknown-linux-gnuinstalling to /home/you/.local/bin  uv  uvxeverything's installed!

    Close the terminal and open a new one, so it can find uv.

  3. Step 3 Install Git and VS Code

    Git saves versions of your work; you start using it in Session 3. On Windows, download it from git-scm.com and keep the installer’s choices as they are. On macOS, typing git --version offers to install it. On Linux, install the git package with your system’s package manager.

    VS Code is the editor you write code in. Download it from code.visualstudio.com, open it, and install the Python extension: open Extensions in the bar on the left, search for Python, and install the one by Microsoft.

    Open a new terminal and check both tools. Your version numbers may be newer:

    uv --versionuv 0.12.23 (x86_64-unknown-linux-gnu)git --versiongit version 2.47.3
  4. Step 4 Tell Git who you are

    Git writes a name and an email into every version you save. Set yours once, with your own name and email, and name the first line of work main, as GitHub does. Each command prints nothing when it works:

    git config --global user.name "Your Name"git config --global user.email "[email protected]"git config --global init.defaultBranch main
  5. Step 5 Make the project

    Go to the folder where you keep your work, such as your home folder, and make a project. --no-package makes a project of plain scripts, the simplest kind; on Day 3 you turn it into a package. --python 3.14 sets the Python version the project uses, and uv downloads that version if your computer doesn’t have it. --description is the line pyproject.toml gives as what the project is.

    uv init --no-package --python 3.14 --description "US equity market data in Python" market-dataInitialized project `market-data` at `/home/you/market-data`cd market-datals -a....git.gitignore.python-versionREADME.mdmain.pypyproject.toml

    uv made a folder, market-data, holding these files:

    FileWhat it is
    .gitGit’s record of every version, kept hidden
    .gitignoreThe files Git should never save
    .python-versionThe Python version this project uses
    README.mdA page that says what the project is
    main.pyA short example program
    pyproject.tomlThe project’s name, its Python version and the packages it needs

    Open the folder in VS Code with File, Open Folder, and delete main.py: right-click it and choose Delete. You won’t need it. Here is what the project’s settings say so far:

    cat .python-version3.14cat pyproject.toml[project]name = "market-data"version = "0.1.0"description = "US equity market data in Python"readme = "README.md"requires-python = ">=3.14"dependencies = []cat .gitignore# Python-generated files__pycache__/*.py[oc]build/dist/wheels/*.egg-info # Virtual environments.venv
  6. Step 6 Add httpx2

    A package is code someone else wrote that your project can use. httpx2 downloads data from the web. Add it:

    uv add httpx2Using CPython 3.14.8 interpreter at: /usr/local/bin/python3.14Creating virtual environment at: .venvResolved 9 packages in 197msPrepared 7 packages in 61msInstalled 7 packages in 64ms + anyio==4.15.1 + h11==0.16.0 + httpcore2==2.13.1 + httpx2==2.13.1 + idna==3.20 + truststore==0.10.4 + typing-extensions==4.16.0cat pyproject.toml[project]name = "market-data"version = "0.1.0"description = "US equity market data in Python"readme = "README.md"requires-python = ">=3.14"dependencies = [    "httpx2>=2.13.1",]
    • uv made .venv, the project’s own Python and packages, kept apart from every other project on your computer.
    • It downloaded httpx2 and the packages httpx2 needs, and wrote httpx2 into pyproject.toml under dependencies.
    • It wrote the exact version of every package to uv.lock. Saving that file with your code lets anyone install exactly the same versions.
  7. Step 7 Get your Alpaca keys

    Alpaca’s market data is free with an account, and the account needs no money. Every account comes with a paper trading account, which trades pretend money; this course uses its keys for market data.

    • Sign up at alpaca.markets with your email, and confirm it from the email Alpaca sends.
    • Log in to your dashboard at app.alpaca.markets, and check that Paper Trading is selected at the top left.
    • On the Home page, find API Keys and choose Generate New Keys.
    • Copy the key ID and the secret key for the next step. Alpaca shows the secret key only once; if you lose it, generate new keys.

    Never paste the secret key into code, a chat or an email.

  8. Step 8 Keep your keys out of Git

    In VS Code, make a file called .env in the market-data folder, and put your two keys in it in place of the stand-ins, with no spaces or quotes:

    .env
    APCA_API_KEY_ID=your-key-idAPCA_API_SECRET_KEY=your-secret-key

    Make .env.example beside it, with the names and stand-in values. It is safe to commit:

    .env.example
    # Copy to .env and set your own values. .env is in .gitignore; this file is not.APCA_API_KEY_ID=your-key-idAPCA_API_SECRET_KEY=your-secret-key

    Then add these two lines to the end of .gitignore, so Git never saves .env:

    .gitignore
    # Your API keys and other settings for this computer only.env

    Print the file to check it. A line starting with # is a comment, a note for people that Git skips:

    cat .gitignore# Python-generated files__pycache__/*.py[oc]build/dist/wheels/*.egg-info # Virtual environments.venv # Your API keys and other settings for this computer only.env
  9. Step 9 Download a quote

    Make a new file in VS Code, quote.py, and type this in:

    quote.py
    import os import httpx2 url = "https://data.alpaca.markets/v2/stocks/snapshots"keys = {    "APCA-API-KEY-ID": os.environ["APCA_API_KEY_ID"],    "APCA-API-SECRET-KEY": os.environ["APCA_API_SECRET_KEY"],}query = {"symbols": "AAPL", "feed": "delayed_sip"}response = httpx2.get(url, params=query, headers=keys, timeout=10)print(response.status_code)snapshot = response.json()["AAPL"]print(snapshot["latestTrade"])print(snapshot["latestQuote"])
    Line 1
    os is part of Python. os.environ holds the program’s environment variables, so your keys can be read from it.
    Line 3
    Load the httpx2 package.
    Line 5
    The address of Alpaca’s snapshots. A snapshot is a share’s latest trade, its latest quote and its bars for the day.
    Lines 6 to 9
    A dictionary of the two headers Alpaca reads your keys from, each filled from an environment variable. If a variable isn’t set, os.environ stops the program with a KeyError naming it.
    Line 10
    The query: the symbols to ask for, and the feed. delayed_sip is the free plan’s feed, every US exchange, 15 minutes behind.
    Line 11
    httpx2.get sends the request, with the query added to the address and the keys as headers, and waits for the answer. timeout=10 gives up after 10 seconds instead of waiting forever.
    Line 12
    Print the answer’s status code: 200 means it worked.
    Line 13
    .json() turns the answer’s JSON text into a Python dictionary of snapshots by ticker, and ["AAPL"] picks out Apple’s.
    Lines 14 to 15
    Print the latest trade, then the latest quote.

    Run it. uv run runs a file with the project’s own Python and packages, and --env-file .env gives it your keys:

    uv run --env-file .env quote.py200{'c': [' ', 'M'], 'i': 52983525029461, 'p': 330.27, 's': 343753, 't': '2026-10-06T20:00:00.000271834Z', 'x': 'Q', 'z': 'C'}{'ap': 330.3, 'as': 200, 'ax': 'Q', 'bp': 330.25, 'bs': 300, 'bx': 'Q', 'c': ['R'], 't': '2026-10-06T20:00:00.001044518Z', 'z': 'C'}

    Shown for the course’s sample data. Yours shows the latest.

    Alpaca keeps its names short, a letter or two each:

    NameWhat it isApple, in the sample
    pThe latest trade’s price330.27
    sHow many shares it traded343,753
    tWhen it traded, in UTC, the time computers keep2026-10-06 at 20:00
    bp, bsThe bid, and how many shares are bid330.25, 300
    ap, asThe ask, and how many shares are offered330.30, 200

    On the sample’s day New York was on daylight saving time, 4 hours behind UTC, so 20:00 in UTC was 16:00 there: the closing trade, as the market shut. Shares also trade before 9:30 and after 16:00, in smaller amounts, so run it in the evening and the latest trade is a later one. x, bx and ax name the exchange each price came from by a letter (Q is Nasdaq), and c and z are codes that describe the trade and the group of exchanges that reported it.

    Python writes 330.30 as 330.3; it is the same number. Alpaca’s data is for your own use, so keep the prices to yourself: don’t publish them on a website.

  10. Step 10 Pick out what matters

    Replace quote.py with this. The new and changed lines are marked:

    quote.py
    import os import httpx2 SNAPSHOTS_URL = "https://data.alpaca.markets/v2/stocks/snapshots"  def get_snapshots(symbols):    """Download each symbol's latest trade, quote and daily bars, 15 minutes behind."""    keys = {        "APCA-API-KEY-ID": os.environ["APCA_API_KEY_ID"],        "APCA-API-SECRET-KEY": os.environ["APCA_API_SECRET_KEY"],    }    query = {"symbols": ",".join(symbols), "feed": "delayed_sip"}    response = httpx2.get(SNAPSHOTS_URL, params=query, headers=keys, timeout=10)    response.raise_for_status()    return response.json()  def describe(symbol, snapshot):    """One line about a symbol: its last price, bid, ask and spread."""    last = snapshot["latestTrade"]["p"]    bid = snapshot["latestQuote"]["bp"]    ask = snapshot["latestQuote"]["ap"]    return (        f"{symbol}  last {last:.2f}  bid {bid:.2f}  ask {ask:.2f}  "        f"spread {ask - bid:.2f}"    )  snapshots = get_snapshots(["AAPL"])print(describe("AAPL", snapshots["AAPL"]))
    Line 5
    The address, in a name in capitals: a setting that never changes while the program runs.
    Lines 8 to 17
    A function: a named piece of code you can call. It takes a list of tickers, joins them with commas (",".join) so one request asks for all of them, stops with an error if the server answered with a problem (raise_for_status), and returns the snapshots by ticker.
    Lines 20 to 28
    A second function turns a snapshot into one line: the last price from the latest trade, the bid and ask from the latest quote, and the spread, ask − bid. :.2f inside an f-string, a string starting with f whose {…} parts are filled in, writes each with two decimal places.
    Lines 31 to 32
    Ask for Apple’s snapshot, with a list of one ticker in square brackets, and print its line.
    uv run --env-file .env quote.pyAAPL  last 330.27  bid 330.25  ask 330.30  spread 0.05

    Shown for the course’s sample data. Yours shows the latest.

    Computers store numbers in binary, so 330.30 − 330.25 comes out as 0.05000000000001137, a hair over 0.05. :.2f prints it as 0.05. Code that moves real money uses exact decimal arithmetic for this reason; for reading quotes, two places are enough.

  11. Step 11 Any ticker

    Change the end of quote.py so it takes the ticker from the command line, and add a docstring at the top that says what the file does:

    quote.py
    """Print a stock's latest quote from Alpaca, 15 minutes behind the market. Run it with a ticker, such as:     uv run --env-file .env quote.py AAPL""" import osimport sys import httpx2 SNAPSHOTS_URL = "https://data.alpaca.markets/v2/stocks/snapshots"  def get_snapshots(symbols):    """Download each symbol's latest trade, quote and daily bars, 15 minutes behind."""    keys = {        "APCA-API-KEY-ID": os.environ["APCA_API_KEY_ID"],        "APCA-API-SECRET-KEY": os.environ["APCA_API_SECRET_KEY"],    }    query = {"symbols": ",".join(symbols), "feed": "delayed_sip"}    response = httpx2.get(SNAPSHOTS_URL, params=query, headers=keys, timeout=10)    response.raise_for_status()    return response.json()  def describe(symbol, snapshot):    """One line about a symbol: its last price, bid, ask and spread."""    last = snapshot["latestTrade"]["p"]    bid = snapshot["latestQuote"]["bp"]    ask = snapshot["latestQuote"]["ap"]    return (        f"{symbol}  last {last:.2f}  bid {bid:.2f}  ask {ask:.2f}  "        f"spread {ask - bid:.2f}"    )  if __name__ == "__main__":    symbol = sys.argv[1] if len(sys.argv) > 1 else "AAPL"    snapshots = get_snapshots([symbol])    if symbol not in snapshots:        sys.exit(f"No quote for {symbol}: check the ticker.")    print(describe(symbol, snapshots[symbol]))
    Lines 1 to 6
    A docstring: what the file does and how to run it, in triple quotes at the top.
    Line 9
    sys holds sys.argv, the list of words on the command line: the file’s name first, then anything typed after it.
    Lines 39 to 41
    This block runs only when you run quote.py itself, not when another file imports it, as the watchlist does in Session 2. It takes the ticker typed after the file’s name, or AAPL if there is none.
    Lines 42 to 43
    Alpaca leaves a ticker it doesn’t know out of its answer, so check before using it. not in is True when the dictionary has no such key. sys.exit with a message prints the message and ends the program with exit code 1, a program’s way of saying it failed.
    uv run --env-file .env quote.py MSFTMSFT  last 522.48  bid 522.40  ask 522.52  spread 0.12

    Shown for the course’s sample data. Yours shows the latest.

    And a ticker that doesn’t exist:

    uv run --env-file .env quote.py ZZZZNo quote for ZZZZ: check the ticker.

    Shown for the course’s sample data. Yours shows the latest.

Session 2 A watchlist of stocks and ETFs

The idea

Step 1 Indexes

An index is one number that follows many shares.The S&P 500 follows 500 large US companies, each counted by its size.size = market value = share price × number of sharesCompany A3,000 bnCompany B2,000 bnCompany C1,000 bnA is worth three times C, so a 1% move in A moves the index three times as much.An index is a number, not something that trades, so its own level has no bid or ask.
01/03

An index is a single number that follows a group of shares, so you can see how a whole market moved at a glance.

The S&P 500 follows 500 large US companies. Each counts in proportion to its market value: its share price times the number of shares it has issued. A company worth twice as much counts twice as much.

In the drawing, company A is worth three times company C, so a 1% rise in A lifts the index three times as much as a 1% rise in C.

An index is measured in points, not dollars. The Nasdaq-100 follows 100 of the largest companies on the Nasdaq exchange. An index is a number worked out from prices, not something that trades, so it has no bid or ask of its own.

Practice

Problem 3

2 points

A share closed yesterday at 80.00 and trades at 82.00 now. By what percent has it moved?

Hint 1

First find the change in dollars.

Hint 2

Divide the change by yesterday’s close, not by today’s price, then multiply by 100.

Solution

change = 82.00 − 80.00 = 2.00

percent = 2.00 ÷ 80.00 × 100

= 2.5%

Which of these can you buy on an exchange?

Show the answer

A: SPY, an ETF that follows the S&P 500 An index is a number worked out from many shares, so there is nothing to buy. An ETF is a fund with its own shares, which trade on an exchange like a company’s.

NVDA rose 2.17 dollars and SPY rose 4.33. Which moved more?

Show the answer

C: NVDA: 1.19% against 0.56% Compare moves in percent, the change as a share of where the price started. In percent NVDA moved about twice as far.

The project, step by step

Build it yourself from this brief, then check it against the steps.

  • Write watchlist.py beside quote.py. It imports get_snapshots from quote.py.
  • Every minute it prints the time, then a table of AAPL, MSFT, NVDA, SPY and QQQ from one request: each symbol’s last price, its change since the previous close in dollars, and that change in percent, in lined-up columns.
  • Ctrl+C stops it with a short message instead of an error.
  1. Step 1 Loop over a list

    Make watchlist.py beside quote.py:

    watchlist.py
    from quote import get_snapshots SYMBOLS = ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"] snapshots = get_snapshots(SYMBOLS)for symbol in SYMBOLS:    snapshot = snapshots[symbol]    print(symbol, snapshot["latestTrade"]["p"], snapshot["prevDailyBar"]["c"])
    Line 1
    Use get_snapshots from quote.py. The if __name__ == "__main__": block you wrote keeps quote.py from printing Apple’s line when it is imported.
    Line 3
    A list: values in square brackets, in order. Three companies’ shares and two ETFs.
    Line 5
    One request for all five snapshots.
    Lines 6 to 8
    A for loop runs its indented lines once for each item in the list, with symbol holding the item. Each line prints the symbol, the last price, and the previous close: the close (c) of the previous day’s bar (prevDailyBar).
    uv run --env-file .env watchlist.pyAAPL 330.27 326.8MSFT 522.48 525.1NVDA 185.12 182.95SPY 771.53 767.2QQQ 752.11 748.9

    Shown for the course’s sample data. Yours shows the latest.

    The next step works out each change and lines everything up.

  2. Step 2 Line up the columns

    watchlist.py
    from datetime import datetime from quote import get_snapshots SYMBOLS = ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"]  def row(symbol, snapshot):    """One table row: symbol, last price, and change since the previous close."""    last = snapshot["latestTrade"]["p"]    previous = snapshot["prevDailyBar"]["c"]    change = last - previous    return f"{symbol:<6} {last:>10.2f} {change:>+8.2f} {change / previous:>+8.2%}"  def show(symbols):    """Print the time, a header, then one row per symbol, from one request."""    snapshots = get_snapshots(symbols)    print(f"\nQuotes at {datetime.now():%H:%M:%S}")    print(f"{'symbol':<6} {'last':>10} {'change':>8} {'%':>8}")    for symbol in symbols:        print(row(symbol, snapshots[symbol]))  show(SYMBOLS)
    Lines 8 to 13
    Work out the change, then lay out the row. Each {…} has a format after its colon: <6 puts the symbol on the left of 6 characters; >10.2f puts a number on the right of 10 characters, with 2 decimal places; a + shows its sign, plus or minus; and % writes a fraction as a percent, so 0.0106 becomes 1.06%.
    Lines 16 to 22
    Fetch every snapshot in one request, then print the time, a header whose widths match the rows, and one row for each symbol. datetime.now() is the time on your computer, and %H:%M:%S writes it as hours, minutes and seconds.
    Line 25
    Call the function once.
    uv run --env-file .env watchlist.py Quotes at 16:15:00symbol       last   change        %AAPL       330.27    +3.47   +1.06%MSFT       522.48    -2.62   -0.50%NVDA       185.12    +2.17   +1.19%SPY        771.53    +4.33   +0.56%QQQ        752.11    +3.21   +0.43%

    Shown for the course’s sample data. Yours shows the latest.

  3. Step 3 Refresh every minute

    watchlist.py
    """A watchlist: the latest quotes for a few symbols, refreshed every minute. Run it, and press Ctrl+C to stop:     uv run --env-file .env watchlist.py""" import timefrom datetime import datetime from quote import get_snapshots SYMBOLS = ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"]REFRESH_SECONDS = 60  def row(symbol, snapshot):    """One table row: symbol, last price, and change since the previous close."""    last = snapshot["latestTrade"]["p"]    previous = snapshot["prevDailyBar"]["c"]    change = last - previous    return f"{symbol:<6} {last:>10.2f} {change:>+8.2f} {change / previous:>+8.2%}"  def show(symbols):    """Print the time, a header, then one row per symbol, from one request."""    snapshots = get_snapshots(symbols)    print(f"\nQuotes at {datetime.now():%H:%M:%S}")    print(f"{'symbol':<6} {'last':>10} {'change':>8} {'%':>8}")    for symbol in symbols:        print(row(symbol, snapshots[symbol]))  if __name__ == "__main__":    try:        while True:            show(SYMBOLS)            time.sleep(REFRESH_SECONDS)    except KeyboardInterrupt:        print("\nStopped.")
    Line 14
    How long to wait between tables, in seconds.
    Lines 36 to 38
    while True repeats forever: show the table, then wait with time.sleep.
    Lines 39 to 40
    Ctrl+C stops a running program by raising a KeyboardInterrupt. try and except catch it, so the program says Stopped instead of printing an error.
    uv run --env-file .env watchlist.py Quotes at 16:15:00symbol       last   change        %AAPL       330.27    +3.47   +1.06%MSFT       522.48    -2.62   -0.50%NVDA       185.12    +2.17   +1.19%SPY        771.53    +4.33   +0.56%QQQ        752.11    +3.21   +0.43% Stopped.

    Shown for the course’s sample data. Yours shows the latest.

    It prints a new table every minute until you press Ctrl+C; here it was stopped after the first. One request a minute is plenty for prices 15 minutes behind: asking faster gains nothing, and Alpaca’s free plan allows 200 requests a minute across everything you run.

Session 3 Git: save your work, and go back to it

The idea

Step 1 Commits

Git keeps your project’s history as commits.A commit is a snapshot of every file, with a message and an id.c824e2bPrint a quote and a watchlisteb65bbeAdd Amazon to the watchlistdfb8805Revert "Add Amazon to the watchlist"HEADEach id is unique; yours will differ. The newest commit is called HEAD.Commit small steps that work, so you can always go back to one.
01/03

Git is a program that keeps the history of a project. Each saved version is a commit: a snapshot of every file, with a message saying what changed, your name, the time, and an id worked out from its contents.

An id is long, so Git shows its first seven characters, enough to tell commits apart. The newest commit on the line of work you are on is called HEAD.

Commit each small step that works. Then a mistake costs minutes, because you can always go back to the last good version.

Practice

You changed watchlist.py, saved it, and want to throw the change away. Which command?

Show the answer

B: git restore watchlist.py git restore puts the file back as it was at the last commit. git revert undoes a commit, and this change was never committed.

Why does .gitignore list the .venv folder?

Show the answer

A: uv can rebuild it from pyproject.toml and uv.lock, so saving it would only add thousands of files .venv is the project’s own Python and packages. The lock file records exactly what is in it, so anyone can rebuild it with uv. Your keys are in .env, which .gitignore lists for its own reason.

What does git revert do to the history?

Show the answer

C: It adds a new commit that undoes the old one, keeping both A revert is itself a commit. Nothing is deleted, so the history shows the change and its undoing.

The project, step by step

Build it yourself from this brief, then check it against the steps.

  • Commit your project with a clear message, and read the history with git log --oneline. Check that .env is not in the commit.
  • Make a change, look at it with git diff, and commit it.
  • Make another change and throw it away with git restore.
  • Undo your last commit with git revert, and read the history again.
  1. Step 1 See what Git sees

    In the market-data folder, ask Git what it sees:

    git statusOn branch main No commits yet Untracked files:  (use "git add <file>..." to include in what will be committed)	.env.example	.gitignore	.python-version	README.md	pyproject.toml	quote.py	uv.lock	watchlist.py nothing added to commit but untracked files present (use "git add" to track)

    uv init already started a Git repository, the folder’s history, on the main branch. Every file is untracked: Git sees it but holds no version of it yet. .venv and .env are missing from the list because .gitignore names them, while .env.example is there to be committed.

  2. Step 2 Save your first version

    git add .git commit -m "Print a quote and a watchlist"[main (root-commit) c824e2b] Print a quote and a watchlist 8 files changed, 211 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 quote.py create mode 100644 uv.lock create mode 100644 watchlist.pygit log --onelinec824e2b Print a quote and a watchlist

    git add . stages every file in the folder that .gitignore doesn’t name; the dot means this folder. The message says what the commit does. git log --oneline shows the commit with its id; yours will differ.

  3. Step 3 Change a file, and save the change

    Add "AMZN", Amazon, to the list in watchlist.py and save the file. Then look at the change and commit it:

    git status --short M watchlist.pygit diffdiff --git a/watchlist.py b/watchlist.pyindex 8f7cfde..b1fc4a9 100644--- a/watchlist.py+++ b/watchlist.py@@ -10,7 +10,7 @@ from datetime import datetime  from quote import get_snapshots -SYMBOLS = ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"]+SYMBOLS = ["AAPL", "MSFT", "NVDA", "AMZN", "SPY", "QQQ"] REFRESH_SECONDS = 60  git commit -am "Add Amazon to the watchlist"[main eb65bbe] Add Amazon to the watchlist 1 file changed, 1 insertion(+), 1 deletion(-)git log --onelineeb65bbe Add Amazon to the watchlistc824e2b Print a quote and a watchlist

    git status --short marks the changed file with M, for modified. git diff shows the old line with − and the new one with +. git commit -am stages every changed file Git already tracks, and commits it, in one step.

  4. Step 4 Throw a change away

    Now change REFRESH_SECONDS to 6, save, and then change your mind:

    git diffdiff --git a/watchlist.py b/watchlist.pyindex b1fc4a9..391e73f 100644--- a/watchlist.py+++ b/watchlist.py@@ -11,7 +11,7 @@ from datetime import datetime from quote import get_snapshots  SYMBOLS = ["AAPL", "MSFT", "NVDA", "AMZN", "SPY", "QQQ"]-REFRESH_SECONDS = 60+REFRESH_SECONDS = 6   def row(symbol, snapshot):git restore watchlist.pygit statusOn branch mainnothing to commit, working tree clean

    git restore put watchlist.py back as it was at the last commit, so git status has nothing to report.

  5. Step 5 Undo a commit

    Say adding Amazon was a mistake. Look at the commit, then revert it:

    git show --stat HEADcommit eb65bbecf372bc64b69cb17201e41232b8f6fd36Author: Your Name <[email protected]>Date:   Wed Oct 7 19:22:20 2026 +0000     Add Amazon to the watchlist  watchlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)git revert --no-edit HEAD[main dfb8805] Revert "Add Amazon to the watchlist" Date: Wed Oct 7 19:22:20 2026 +0000 1 file changed, 1 insertion(+), 1 deletion(-)git log --onelinedfb8805 Revert "Add Amazon to the watchlist"eb65bbe Add Amazon to the watchlistc824e2b Print a quote and a watchlist

    git show --stat HEAD shows the newest commit and which files it changed. git revert makes a new commit that takes Amazon back out. The log keeps all three commits: the first version, the change, and its undoing.

Walkthrough

The whole solution, explained line by line. Open it once you have tried.

Show the walkthrough

The finished quote.py:

quote.py
"""Print a stock's latest quote from Alpaca, 15 minutes behind the market. Run it with a ticker, such as:     uv run --env-file .env quote.py AAPL""" import osimport sys import httpx2 SNAPSHOTS_URL = "https://data.alpaca.markets/v2/stocks/snapshots"  def get_snapshots(symbols):    """Download each symbol's latest trade, quote and daily bars, 15 minutes behind."""    keys = {        "APCA-API-KEY-ID": os.environ["APCA_API_KEY_ID"],        "APCA-API-SECRET-KEY": os.environ["APCA_API_SECRET_KEY"],    }    query = {"symbols": ",".join(symbols), "feed": "delayed_sip"}    response = httpx2.get(SNAPSHOTS_URL, params=query, headers=keys, timeout=10)    response.raise_for_status()    return response.json()  def describe(symbol, snapshot):    """One line about a symbol: its last price, bid, ask and spread."""    last = snapshot["latestTrade"]["p"]    bid = snapshot["latestQuote"]["bp"]    ask = snapshot["latestQuote"]["ap"]    return (        f"{symbol}  last {last:.2f}  bid {bid:.2f}  ask {ask:.2f}  "        f"spread {ask - bid:.2f}"    )  if __name__ == "__main__":    symbol = sys.argv[1] if len(sys.argv) > 1 else "AAPL"    snapshots = get_snapshots([symbol])    if symbol not in snapshots:        sys.exit(f"No quote for {symbol}: check the ticker.")    print(describe(symbol, snapshots[symbol]))
Lines 1 to 6
The docstring: what the file does and how to run it.
Lines 8 to 11
Load os, for the environment, sys, for the command line, and httpx2, for web requests.
Line 13
The address of Alpaca’s snapshots.
Lines 16 to 25
Fetch a list of symbols’ snapshots in one request: your keys from the environment as headers, the symbols joined with commas and the free plan’s feed as the query, giving up after 10 seconds and stopping on an error.
Lines 28 to 36
Turn a snapshot into one line: the last price, the bid, the ask, and the spread, ask − bid, each to two decimal places.
Lines 39 to 44
Only when the file is run: take the ticker from the command line, AAPL if there is none, stop with a message if Alpaca doesn’t know it, and print its line.

The finished watchlist.py:

watchlist.py
"""A watchlist: the latest quotes for a few symbols, refreshed every minute. Run it, and press Ctrl+C to stop:     uv run --env-file .env watchlist.py""" import timefrom datetime import datetime from quote import get_snapshots SYMBOLS = ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"]REFRESH_SECONDS = 60  def row(symbol, snapshot):    """One table row: symbol, last price, and change since the previous close."""    last = snapshot["latestTrade"]["p"]    previous = snapshot["prevDailyBar"]["c"]    change = last - previous    return f"{symbol:<6} {last:>10.2f} {change:>+8.2f} {change / previous:>+8.2%}"  def show(symbols):    """Print the time, a header, then one row per symbol, from one request."""    snapshots = get_snapshots(symbols)    print(f"\nQuotes at {datetime.now():%H:%M:%S}")    print(f"{'symbol':<6} {'last':>10} {'change':>8} {'%':>8}")    for symbol in symbols:        print(row(symbol, snapshots[symbol]))  if __name__ == "__main__":    try:        while True:            show(SYMBOLS)            time.sleep(REFRESH_SECONDS)    except KeyboardInterrupt:        print("\nStopped.")
Lines 1 to 6
The docstring.
Lines 8 to 11
Load the clock, for waiting; the date and time, for the header; and get_snapshots from quote.py.
Lines 13 to 14
The settings: which symbols to show, and how often.
Lines 17 to 22
One row: the symbol on the left, then the last price, the change and the percent change on the right of their columns, signed.
Lines 25 to 31
One table from one request: the time, a header, and a row for each symbol.
Lines 34 to 40
Show a table, wait, and repeat, until Ctrl+C, which prints Stopped.

Go deeper

  • The shell, git and a debuggerThe shell, paths through folders, Git’s branches, and a debugger that stops inside your code, each worked through on a small project.

Check yourself

Questions an interviewer could ask about today’s work.

  1. 01What is the bid–ask spread, and who earns it?Show answer

    The gap between the lowest price a seller will take, the ask, and the highest a buyer will pay, the bid. Market makers, the firms that post both prices all day, earn it: they buy at the bid and sell at the ask, and the spread pays them for the risk of holding shares in between.

  2. 02Share A is quoted at 100.00 bid, 100.10 ask. Share B is quoted at 50.00 bid, 50.10 ask. Which is cheaper to trade?Show answer

    Both spreads are 0.10 dollars, but they are compared as a share of the mid price, the halfway point between bid and ask. A’s is 0.10 ÷ 100.05 × 100 = 0.10% and B’s is 0.10 ÷ 50.05 × 100 = 0.20%, so A is cheaper to trade.

  3. 03What is the difference between an index and an ETF?Show answer

    An index is a number worked out from a list of shares, such as the S&P 500 from 500 large US companies; you can’t buy it. An ETF is a fund that holds those shares and whose own shares trade on an exchange, so you can buy it. SPY’s price follows the S&P 500.

  4. 04Where should an API key live, and why not in your code?Show answer

    In a file such as .env that Git ignores, or in a server’s secret settings. Code is copied, committed and pushed to GitHub, and a key in it is public from that moment: anyone who finds it can use your account. List the names in a committed .env.example, without the values. If a key ever reaches Git, make a new one, because deleting the line leaves it in the history.

  5. 05Your program asks a server for prices. What can go wrong, and what does today’s code do about it?Show answer

    The server can be slow, can refuse your keys, or can answer with an error. Today’s code gives up after 10 seconds (the timeout) and stops with a clear error if the answer is not OK (raise_for_status), such as 401 when a key is wrong. Day 6 adds retries.

  6. 06Why commit uv.lock but not .venv?Show answer

    uv.lock records the exact version of every package, so anyone can rebuild the same environment from it. .venv is that environment itself: thousands of files uv can rebuild, so it stays out of Git.

Learning points

  • A quote has a bid, the most a buyer will pay; an ask, the least a seller will take; and the spread between them, the cost of trading at once.
  • Alpaca’s free plan is 15 minutes behind the market: right for learning and daily decisions, not for trading in the moment.
  • API keys live in .env, which .gitignore names; .env.example lists their names for everyone else.
  • uv gives each project its own Python and packages: pyproject.toml lists them and uv.lock pins their exact versions.
  • Split a program into a function that fetches and a function that formats: the second is easy to check, and other files can import both.
  • Commit small steps that work, with clear messages; git restore and git revert take you back.

Keep going

The hardest part of Day 1 is the setup

If an install failed or a command was not found, you are in good company: setting up a machine trips up experienced engineers too. Read the last line of the error, search for it word for word, and try again. Every fix you make now is one you won’t need to make again.

From tomorrow the setup is done, and each day starts with code.

Ship it

Run git status in the market-data folder. It should end with “nothing to commit, working tree clean”: today’s work is saved, and your keys are not in it.

On Day 4 this repository goes on GitHub, as the first project on your portfolio site.

For education only. Not investment advice. Terms of Use