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.
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: 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 pointsA 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 pointsYou 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
.envasAPCA_API_KEY_IDandAPCA_API_SECRET_KEY, list the names in.env.example, and add.envto.gitignore. - Write quote.py. It takes a ticker from the command line, AAPL if there is none, asks
https://data.alpaca.markets/v2/stocks/snapshotsfor its snapshot (the querysymbols=AAPL&feed=delayed_sip, with your keys as the headersAPCA-API-KEY-IDandAPCA-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 AAPLanduv run --env-file .env quote.py MSFT.
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.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 | shOn 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.
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 --versionoffers to install it. On Linux, install thegitpackage 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
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
Step 5 Make the project
Go to the folder where you keep your work, such as your home folder, and make a project.
--no-packagemakes a project of plain scripts, the simplest kind; on Day 3 you turn it into a package.--python 3.14sets the Python version the project uses, and uv downloads that version if your computer doesn’t have it.--descriptionis 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:
File What it is .git Git’s record of every version, kept hidden .gitignore The files Git should never save .python-version The Python version this project uses README.md A page that says what the project is main.py A short example program pyproject.toml The 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
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.tomlunder 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.
- uv made
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.
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:
.envAPCA_API_KEY_ID=your-key-idAPCA_API_SECRET_KEY=your-secret-keyMake .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-keyThen 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.envPrint 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
Step 9 Download a quote
Make a new file in VS Code, quote.py, and type this in:
quote.pyimport 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
osis part of Python.os.environholds 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.environstops the program with a KeyError naming it. - Line 10
- The query: the symbols to ask for, and the feed.
delayed_sipis the free plan’s feed, every US exchange, 15 minutes behind. - Line 11
httpx2.getsends the request, with the query added to the address and the keys as headers, and waits for the answer.timeout=10gives 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 runruns a file with the project’s own Python and packages, and--env-file .envgives 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:
Name What it is Apple, in the sample p The latest trade’s price 330.27 s How many shares it traded 343,753 t When it traded, in UTC, the time computers keep 2026-10-06 at 20:00 bp, bs The bid, and how many shares are bid 330.25, 300 ap, as The ask, and how many shares are offered 330.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.
Step 10 Pick out what matters
Replace quote.py with this. The new and changed lines are marked:
quote.pyimport 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.
:.2finside 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.
:.2fprints it as 0.05. Code that moves real money uses exact decimal arithmetic for this reason; for reading quotes, two places are enough.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
sysholdssys.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 inis True when the dictionary has no such key.sys.exitwith 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 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 pointsA 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_snapshotsfrom 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.
Step 1 Loop over a list
Make watchlist.py beside quote.py:
watchlist.pyfrom 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_snapshotsfrom quote.py. Theif __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
symbolholding 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.
Step 2 Line up the columns
watchlist.pyfrom 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:<6puts the symbol on the left of 6 characters;>10.2fputs 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:%Swrites 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.
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 Truerepeats forever: show the table, then wait withtime.sleep.- Lines 39 to 40
- Ctrl+C stops a running program by raising a KeyboardInterrupt.
tryandexceptcatch 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 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.
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 initalready 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..venvand.envare missing from the list because.gitignorenames them, while.env.exampleis there to be committed.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.gitignoredoesn’t name; the dot means this folder. The message says what the commit does.git log --onelineshows the commit with its id; yours will differ.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 --shortmarks the changed file with M, for modified.git diffshows the old line with − and the new one with +.git commit -amstages every changed file Git already tracks, and commits it, in one step.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 restoreput watchlist.py back as it was at the last commit, sogit statushas nothing to report.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 HEADshows the newest commit and which files it changed.git revertmakes 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:
"""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:
"""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_snapshotsfrom 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.
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.
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.
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.
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.
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.
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.tomllists them anduv.lockpins 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 restoreandgit reverttake 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