Congressional Stock Tracker
Fetch real congressional trade data and display it in a web dashboard.
Introduction
30 Second Summary
Every stock trade made by a member of Congress is public record. These disclosures sit in open datasets, waiting to be turned into something useful.
In this project, you will build a congressional stock tracker that fetches real trade data, processes it with Python, and displays the results on a web dashboard you can view in your browser.
What You'll Build
A complete data pipeline that pulls financial disclosure data from public government datasets, filters and sorts it, and renders it as an interactive table in your browser.
By the end of this project, you'll have:
- A Python script that fetches and normalizes stock trade data from both the Senate and House of Representatives.
- A JSON export of processed trades that feeds into an interactive web page.
- An HTML dashboard with styled tables, chamber filtering, and column sorting powered by JavaScript.
- Secret Mission: A live search bar to find trades by politician name or stock ticker.
Are there any prerequisites?
None. This guide includes setup for all required tools. You just need a computer with macOS or Windows.
Before We Start
Members of the U.S. Congress are required to publicly disclose their stock trades. This data is freely available online, which means anyone can track what politicians are buying and selling.
In this step, you will set up your development environment. You will install a VS Code extension for serving web pages locally, create a project folder with a Python virtual environment, and install the packages your project needs.
In this step, get ready to:
- Install the Live Server extension in VS Code.
- Create a project folder with a Python virtual environment.
- Install Python packages and verify the setup works.
Install the Live Server extension in VS Code
Later in this project, you will build an HTML dashboard that displays stock trade data. Live Server is a VS Code extension that serves your HTML files on a local web server and auto-refreshes the browser whenever you save changes.
- Press Cmd+Space to open Spotlight Search.
- Type Visual Studio Code and press Enter to open it.
- Open the Extensions panel by pressing Cmd+Shift+X.
- Type Live Server in the search bar at the top of the panel.
- Find the result by Ritwick Dey (it will have millions of installs).
- Click Install.
What is Live Server?
Live Server launches a local development server that serves your HTML files at http://localhost:5500. When you save a change to your HTML or CSS, the browser automatically refreshes to show the update.
Without it, you would have to manually refresh the browser every time you edit your dashboard.
Don't see the Install button?
If you see Uninstall instead, the extension is already installed. You are good to go.
Make sure you pick the one by Ritwick Dey. There are several extensions with similar names.
Help me find the right Live Server extension in VS Code.
Create your project folder and virtual environment
A virtual environment is an isolated space where Python packages are installed separately from your system Python. This keeps your project's dependencies contained and avoids conflicts with other projects.
- Open Terminal by pressing Cmd+Space, typing Terminal, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir congress-tracker
cd congress-tracker
- Create a Python virtual environment by running:
python3 -m venv venv
- Activate the virtual environment by running:
source venv/bin/activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
What does (venv) mean?
The (venv) prefix in your terminal tells you that any pip install commands will install packages into this isolated environment, not your system Python.
When you close the terminal or run deactivate, the environment turns off. You will need to reactivate it next time you work on this project.
Don't see (venv) in your prompt?
Make sure you ran source venv/bin/activate (not just venv/bin/activate without source).
Confirm you are inside the congress-tracker folder by running pwd. You should see a path ending in /Desktop/congress-tracker.
Help me activate my Python virtual environment.
Install packages and verify your setup
Your project needs the requests library to fetch data from the internet and pandas for data processing.
- Install both packages by running:
pip3 install requests==2.32.3 pandas==2.2.3
Why pin specific versions?
The == syntax tells pip to install exact versions. This ensures your project behaves the same way regardless of when you run the install.
You should see output ending with a success message listing the installed packages.
- Verify your setup by creating a quick test script. Run this command to create the file:
touch test_setup.py
- Open the congress-tracker folder in VS Code. In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the congress-tracker folder on your Desktop, then click Open.
- In the Explorer sidebar on the left, click test_setup.py to open it.
- Paste the following code into test_setup.py:
import requests
# Test that the requests library works and the data endpoint is accessible
response = requests.get("https://house-stock-watcher-data.s3-us-west-2.amazonaws.com/data/all_transactions.json")
print(f"Status code: {response.status_code}")
print(f"Number of trades fetched: {len(response.json())}")
print("Setup complete! Your environment is ready.")
What does this script do?
This script imports requests and tries to fetch real congressional trade data from a public dataset hosted on S3. If it prints a status code of 200 and a trade count, everything is installed and accessible.
- Save the file by pressing Cmd+S.
- Switch back to your Terminal and run the test script:
python3 test_setup.py
You should see output similar to this:
Status code: 200
Number of trades fetched: 18543
Setup complete! Your environment is ready.
What should I see?
The exact number of trades will vary because the dataset updates over time. The important thing is that you see Status code: 200 and a number greater than zero.
A status code of 200 means the request succeeded. This confirms that requests is installed correctly and the data endpoint is reachable from your machine.
Seeing ModuleNotFoundError: No module named 'requests'?
This means your virtual environment is not active. Run source venv/bin/activate first, then try python3 test_setup.py again.
If you still see the error, make sure you ran pip3 install requests==2.32.3 while (venv) was showing in your prompt.
Help me fix the ModuleNotFoundError for requests.
Your environment is ready. Next up, you will write a Python script that fetches trade data from both the Senate and House of Representatives and normalizes it into a single format.
Fetch Congressional Trade Data
Your environment is ready. Now it's time to pull real data.
Members of Congress are required to publicly disclose their stock trades under the STOCK Act. Several community projects collect this disclosure data and publish it as publicly accessible JSON files on Amazon S3. In this step, you'll write a Python script that fetches trade data from both the House and Senate datasets.
In this step, get ready to:
- Fetch House of Representatives trade data from a public S3 dataset.
- Fetch Senate trade data from a separate S3 dataset.
- Normalize both datasets into a common format and verify they combine correctly.
Create the script and fetch House trades
The House Stock Watcher project publishes every disclosed House trade as a single JSON file on S3. Your script will download this file and reshape each trade record into a standard dictionary format.
- In VS Code, create a new file called fetch_trades.py inside your project folder.
- Paste the following code into fetch_trades.py:
import requests
import json
from datetime import datetime
HOUSE_URL = "https://house-stock-watcher-data.s3-us-west-2.amazonaws.com/data/all_transactions.json"
SENATE_URL = "https://senate-stock-watcher-data.s3-us-west-2.amazonaws.com/aggregate/all_transactions.json"
def fetch_house_trades():
"""Fetch House of Representatives trade data."""
print("Fetching House trades...")
response = requests.get(HOUSE_URL)
if response.status_code != 200:
print("Failed to fetch House data.")
return []
trades = response.json()
normalized = []
for trade in trades:
normalized.append({
"politician": trade.get("representative", "Unknown"),
"chamber": "House",
"ticker": trade.get("ticker", "--"),
"asset_description": trade.get("asset_description", ""),
"type": trade.get("type", ""),
"amount": trade.get("amount", ""),
"transaction_date": trade.get("transaction_date", ""),
"disclosure_date": trade.get("disclosure_date", ""),
})
return normalized
What does this code do?
- The two URL constants point to publicly hosted JSON files containing all disclosed trades from the House and Senate.
- requests.get(HOUSE_URL) downloads the entire House dataset as JSON. If the request fails, the function returns an empty list.
- The for loop reshapes each trade into a common dictionary. Notice it pulls the politician name from the representative field, which is specific to the House dataset.
Why normalize the data?
The House dataset uses the field name representative for the politician, while the Senate dataset uses senator. By mapping both to a common politician key, you can later combine and filter trades from both chambers without worrying about different field names.
- Save fetch_trades.py.
You won't see any output yet. The next substep adds the Senate function, then you'll test both together.
Add the Senate trades fetcher
Senate trade data lives in a separate S3 bucket with its own field naming conventions. You'll add a second function that follows the same pattern but pulls from the Senate endpoint.
- In fetch_trades.py, add the following function below fetch_house_trades():
def fetch_senate_trades():
"""Fetch Senate trade data."""
print("Fetching Senate trades...")
response = requests.get(SENATE_URL)
if response.status_code != 200:
print("Failed to fetch Senate data.")
return []
trades = response.json()
normalized = []
for trade in trades:
normalized.append({
"politician": trade.get("senator", "Unknown"),
"chamber": "Senate",
"ticker": trade.get("ticker", "--"),
"asset_description": trade.get("asset_description", ""),
"type": trade.get("type", ""),
"amount": trade.get("amount", ""),
"transaction_date": trade.get("transaction_date", ""),
"disclosure_date": trade.get("disclosure_date", ""),
})
return normalized
What's different about this function?
The structure is identical to fetch_house_trades(), with two key differences.
- It reads from SENATE_URL instead of HOUSE_URL.
- It maps the politician name from the senator field instead of representative. Both functions output the same dictionary shape, so the results can be combined seamlessly.
- Save fetch_trades.py.
You won't see a change yet. The next substep runs both functions to confirm everything works.
Test the combined data
Both functions now produce identically shaped dictionaries. Combining them is as simple as adding two Python lists together.
- Test both functions by running this command in your terminal:
python3 -c "from fetch_trades import fetch_house_trades, fetch_senate_trades; h = fetch_house_trades(); s = fetch_senate_trades(); print(f'House: {len(h)}, Senate: {len(s)}, Combined: {len(h+s)}')"
You should see output showing the number of trades fetched from each chamber, plus a combined total in the thousands.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
- Confirm you're running the command from inside your project folder (the same folder that contains fetch_trades.py).
- If you see an error about requests not being found, re-run pip3 install requests==2.32.3 inside your activated environment.
help me debug a ModuleNotFoundError when importing fetch_trades
✔️ Awesome, I've got everything!
Great. Double check you've saved your file, then move on to the next step.
ⓧ I'd like to double check the full code
Here's what your complete fetch_trades.py should look like at this point:
import requests
import json
from datetime import datetime
HOUSE_URL = "https://house-stock-watcher-data.s3-us-west-2.amazonaws.com/data/all_transactions.json"
SENATE_URL = "https://senate-stock-watcher-data.s3-us-west-2.amazonaws.com/aggregate/all_transactions.json"
def fetch_house_trades():
"""Fetch House of Representatives trade data."""
print("Fetching House trades...")
response = requests.get(HOUSE_URL)
if response.status_code != 200:
print("Failed to fetch House data.")
return []
trades = response.json()
normalized = []
for trade in trades:
normalized.append({
"politician": trade.get("representative", "Unknown"),
"chamber": "House",
"ticker": trade.get("ticker", "--"),
"asset_description": trade.get("asset_description", ""),
"type": trade.get("type", ""),
"amount": trade.get("amount", ""),
"transaction_date": trade.get("transaction_date", ""),
"disclosure_date": trade.get("disclosure_date", ""),
})
return normalized
def fetch_senate_trades():
"""Fetch Senate trade data."""
print("Fetching Senate trades...")
response = requests.get(SENATE_URL)
if response.status_code != 200:
print("Failed to fetch Senate data.")
return []
trades = response.json()
normalized = []
for trade in trades:
normalized.append({
"politician": trade.get("senator", "Unknown"),
"chamber": "Senate",
"ticker": trade.get("ticker", "--"),
"asset_description": trade.get("asset_description", ""),
"type": trade.get("type", ""),
"amount": trade.get("amount", ""),
"transaction_date": trade.get("transaction_date", ""),
"disclosure_date": trade.get("disclosure_date", ""),
})
return normalized
Your script can now pull real congressional trade data from two public sources and combine them into a single list. Next up, you'll filter, sort, and export this data so your web dashboard can display it.
Process and Export to JSON
In the previous step, you fetched thousands of raw trade records from both the Senate and House datasets. The raw data contains plenty of noise though.
Many entries have missing ticker symbols or represent non-stock transactions. In this step, you'll filter out the noise, sort the remaining trades by date, and export a clean JSON file that your web dashboard can load.
In this step, get ready to:
- Filter out trades with invalid tickers and non-stock transactions.
- Sort remaining trades by disclosure date and limit to 100 records.
- Export processed data to a trades.json file.
Add a date parsing helper
The disclosure dates in the datasets come in different formats. Some use MM/DD/YYYY, others use YYYY-MM-DD. Python needs a consistent way to compare these dates for sorting.
- In fetch_trades.py, scroll down to below the fetch_senate_trades() function and add the following date parsing helper:
def parse_date(date_string):
"""Try to parse a date string in common formats."""
formats = ["%m/%d/%Y", "%Y-%m-%d", "%m/%d/%y"]
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except (ValueError, TypeError):
continue
return datetime.min
What does this code do?
- formats lists three common date patterns the datasets might use.
- The function tries each format in order. If one works, it returns the parsed date immediately.
- datetime.min is Python's earliest possible date (year 1, January 1). If no format matches, the trade gets pushed to the end when sorting.
- Save fetch_trades.py.
- You won't see a change yet. The next function uses this helper to sort trades by date.
Filter and sort the trade data
Now you need a function that cleans up the combined trade list. It removes entries with placeholder tickers like -- or N/A, keeps only actual stock purchases and sales, sorts by disclosure date, and returns the 100 most recent.
- Below the parse_date() function you just added, paste the following process_trades() function:
def process_trades(trades):
"""Filter and sort trades."""
# Filter out trades with no valid ticker
filtered = [t for t in trades if t["ticker"] not in ("--", "", None, "N/A")]
# Filter to only stock-type transactions (purchases and sales)
filtered = [t for t in filtered if t["type"] in (
"Purchase", "Sale", "Sale (Full)", "Sale (Partial)",
"purchase", "sale", "sale_full", "sale_partial"
)]
# Sort by disclosure date (most recent first)
filtered.sort(key=lambda t: parse_date(t["disclosure_date"]), reverse=True)
# Return the most recent 100 trades
return filtered[:100]
What does this code do?
- The first filter removes trades where the ticker is --, empty, None, or N/A. These represent non-stock assets like mutual funds or bonds that don't have a stock ticker.
- The second filter keeps only purchase and sale transactions. The Senate and House datasets use slightly different capitalization, so both forms are included.
- filtered.sort() uses your parse_date() helper to sort by disclosure date with the most recent trades first.
- filtered[:100] limits the output to 100 trades. This keeps the JSON file small enough for the browser to render quickly.
- Save fetch_trades.py.
- You won't see a change yet. The next function will tie everything together and produce the output file.
Run the pipeline and export to JSON
The main() function ties everything together. It calls both fetch functions, combines the results, processes them, and writes the output to trades.json.
- Below process_trades(), add the main() function and the script entry point:
def main():
house_trades = fetch_house_trades()
senate_trades = fetch_senate_trades()
print(f"Fetched {len(house_trades)} House trades and {len(senate_trades)} Senate trades.")
all_trades = house_trades + senate_trades
processed = process_trades(all_trades)
print(f"After filtering: {len(processed)} trades to display.")
with open("trades.json", "w") as f:
json.dump(processed, f, indent=2)
print("Data saved to trades.json")
if __name__ == "__main__":
main()
What does this code do?
- fetch_house_trades() and fetch_senate_trades() pull data from the public S3 datasets you set up in the previous step.
- The two lists are combined with +, then passed through process_trades() to filter and sort.
- json.dump() writes the processed list to trades.json with indent=2 for readability.
- if __name__ == "__main__" ensures the script only runs when executed directly, not when imported as a module.
- Save fetch_trades.py.
- Run the script by entering this command in your terminal:
python3 fetch_trades.py
- You should see output similar to this:
Fetching House trades...
Fetching Senate trades...
Fetched XXXXX House trades and XXXX Senate trades.
After filtering: 100 trades to display.
Data saved to trades.json
What should I see?
The exact numbers will vary depending on when you run the script, because the datasets are updated as new disclosures are filed. The important thing is that both fetches succeed (no "Failed" messages) and the final count shows 100 trades after filtering.
- Confirm the output file exists by running:
ls trades.json
- You should see trades.json listed in the output.
Seeing a "Failed to fetch" message?
- Check your internet connection. The script needs to reach the public S3 endpoints to download data.
- Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
- If you see a ModuleNotFoundError for requests, re-run pip3 install requests==2.32.3 inside your activated virtual environment.
Help me debug why my script isn't fetching data or producing trades.json
✔️ Awesome, I've got everything!
Great. Make sure you've saved fetch_trades.py and confirmed that trades.json exists in your project folder.
ⓧ I'd like to double check the full code
Here is the complete fetch_trades.py file at this point:
import requests
import json
from datetime import datetime
HOUSE_URL = "https://house-stock-watcher-data.s3-us-west-2.amazonaws.com/data/all_transactions.json"
SENATE_URL = "https://senate-stock-watcher-data.s3-us-west-2.amazonaws.com/aggregate/all_transactions.json"
def fetch_house_trades():
"""Fetch House of Representatives trade data."""
print("Fetching House trades...")
response = requests.get(HOUSE_URL)
if response.status_code != 200:
print("Failed to fetch House data.")
return []
trades = response.json()
normalized = []
for trade in trades:
normalized.append({
"politician": trade.get("representative", "Unknown"),
"chamber": "House",
"ticker": trade.get("ticker", "--"),
"asset_description": trade.get("asset_description", ""),
"type": trade.get("type", ""),
"amount": trade.get("amount", ""),
"transaction_date": trade.get("transaction_date", ""),
"disclosure_date": trade.get("disclosure_date", ""),
})
return normalized
def fetch_senate_trades():
"""Fetch Senate trade data."""
print("Fetching Senate trades...")
response = requests.get(SENATE_URL)
if response.status_code != 200:
print("Failed to fetch Senate data.")
return []
trades = response.json()
normalized = []
for trade in trades:
normalized.append({
"politician": trade.get("senator", "Unknown"),
"chamber": "Senate",
"ticker": trade.get("ticker", "--"),
"asset_description": trade.get("asset_description", ""),
"type": trade.get("type", ""),
"amount": trade.get("amount", ""),
"transaction_date": trade.get("transaction_date", ""),
"disclosure_date": trade.get("disclosure_date", ""),
})
return normalized
def parse_date(date_string):
"""Try to parse a date string in common formats."""
formats = ["%m/%d/%Y", "%Y-%m-%d", "%m/%d/%y"]
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except (ValueError, TypeError):
continue
return datetime.min
def process_trades(trades):
"""Filter and sort trades."""
# Filter out trades with no valid ticker
filtered = [t for t in trades if t["ticker"] not in ("--", "", None, "N/A")]
# Filter to only stock-type transactions (purchases and sales)
filtered = [t for t in filtered if t["type"] in (
"Purchase", "Sale", "Sale (Full)", "Sale (Partial)",
"purchase", "sale", "sale_full", "sale_partial"
)]
# Sort by disclosure date (most recent first)
filtered.sort(key=lambda t: parse_date(t["disclosure_date"]), reverse=True)
# Return the most recent 100 trades
return filtered[:100]
def main():
house_trades = fetch_house_trades()
senate_trades = fetch_senate_trades()
print(f"Fetched {len(house_trades)} House trades and {len(senate_trades)} Senate trades.")
all_trades = house_trades + senate_trades
processed = process_trades(all_trades)
print(f"After filtering: {len(processed)} trades to display.")
with open("trades.json", "w") as f:
json.dump(processed, f, indent=2)
print("Data saved to trades.json")
if __name__ == "__main__":
main()
Your data pipeline is complete. You now have a clean trades.json file with the 100 most recent congressional stock trades. Next up, you'll build an HTML dashboard that loads this file and displays the trades in a styled table.
Build the Web Dashboard
Your Python script is fetching real congressional trade data and saving it to a trades.json file. Now you need a way to actually see that data.
In this step, you will build an HTML page that reads trades.json and displays the trades in a styled, readable table. The page uses CSS for a dark theme with color-coded trades and JavaScript to load the data dynamically.
In this step, get ready to:
- Create an HTML file with a table structure for displaying trades.
- Add CSS styling for a dark theme with alternating row colors.
- Write JavaScript that loads trade data and populates the table.
Create the HTML file with table structure
The HTML defines the structure of your dashboard. It includes a page title, subtitle, filter buttons, and a table with column headers for each piece of trade data.
- Create a new file called index.html in your project folder (the same folder that contains fetch_trades.py and trades.json).
- Paste the following HTML structure into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Congressional Stock Tracker</title>
<style>
</style>
</head>
<body>
<h1>Congressional Stock Tracker</h1>
<p class="subtitle">Real financial disclosure data from the U.S. Senate and House of Representatives</p>
<div class="filters">
<button class="filter-btn active" data-chamber="all">All</button>
<button class="filter-btn" data-chamber="Senate">Senate</button>
<button class="filter-btn" data-chamber="House">House</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th data-sort="politician">Politician</th>
<th data-sort="chamber">Chamber</th>
<th data-sort="ticker">Ticker</th>
<th data-sort="type">Type</th>
<th data-sort="amount">Amount</th>
<th data-sort="transaction_date">Trade Date</th>
<th data-sort="disclosure_date">Disclosed</th>
</tr>
</thead>
<tbody id="trades-body"></tbody>
</table>
</div>
<script>
</script>
</body>
</html>
What does this HTML do?
The <style> and <script> tags are empty placeholders for now. You will fill them in the next two substeps.
The <tbody id="trades-body"> element is where JavaScript will inject rows of trade data. The filter buttons and table headers include data- attributes that your code will use later to power interactivity.
- Save index.html.
- Right-click index.html in the VS Code Explorer sidebar and select Open with Live Server.
- Your browser opens and you should see the heading "Congressional Stock Tracker", a subtitle, three filter buttons (All, Senate, House), and an empty table with column headers.
Don't see the Go Live or Open with Live Server option?
Make sure your project folder is open in VS Code (not just a single file). Go to File then Open Folder and select your project folder. The Live Server extension only activates when a folder is open.
Help me get Live Server working in VS Code.
Add CSS styling for the dark theme
The page is functional but unstyled. CSS transforms it into a dark-themed dashboard with readable rows, color-coded trade types, and distinct chamber labels.
- In index.html, find the empty <style> tag inside <head> and paste the following CSS between the opening and closing <style> tags:
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #1a1a2e; color: #e0e0e0; padding: 20px; }
h1 { text-align: center; margin-bottom: 10px; color: #ffffff; font-size: 1.8rem; }
.subtitle { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; }
.filters { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
.filter-btn { padding: 8px 20px; border: 1px solid #444; background-color: #2a2a3e; color: #e0e0e0; border-radius: 4px; cursor: pointer; font-size: 0.9rem; }
.filter-btn.active { background-color: #4a90d9; border-color: #4a90d9; color: #ffffff; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { background-color: #2a2a3e; padding: 12px 10px; text-align: left; cursor: pointer; user-select: none; border-bottom: 2px solid #444; }
th:hover { background-color: #3a3a4e; }
td { padding: 10px; border-bottom: 1px solid #333; }
tr:nth-child(even) { background-color: #22223a; }
tr:hover { background-color: #2a2a4e; }
.purchase { color: #4caf50; }
.sale { color: #f44336; }
.chamber-tag { padding: 2px 8px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
.chamber-senate { background-color: #1e3a5f; color: #64b5f6; }
.chamber-house { background-color: #3e2723; color: #ffab91; }
What do these styles do?
- The body style sets a dark navy background (#1a1a2e) with light text for readability.
- The tr:nth-child(even) rule creates alternating row colors so your eyes can track across long rows.
- The .purchase and .sale classes color-code trade types green and red.
- The .chamber-senate and .chamber-house classes create distinct badge-style labels for each chamber.
- Save index.html and check your browser.
- The page should now have a dark background, a centered white heading, a gray subtitle, styled filter buttons (the "All" button highlighted in blue), and a dark table header row.
Page still looks unstyled?
Make sure you pasted the CSS between the <style> and </style> tags (not outside them). Check that you did not accidentally delete the </style> closing tag.
Help me fix my CSS styling in index.html.
Write JavaScript to load and render trades
The table is styled but empty. JavaScript reads the trades.json file (generated by your Python script) and fills the table with real data.
- In index.html, find the empty <script> tag at the bottom of the <body> and paste the following JavaScript between the opening and closing <script> tags:
let allTrades = [];
let currentChamber = "all";
let currentSort = { key: "disclosure_date", ascending: false };
async function loadTrades() {
try {
const response = await fetch("trades.json");
allTrades = await response.json();
renderTrades();
} catch (error) {
document.getElementById("trades-body").innerHTML =
'<tr><td colspan="7" style="text-align:center;padding:20px;">Run fetch_trades.py first to generate trades.json</td></tr>';
}
}
function renderTrades() {
let trades = allTrades;
if (currentChamber !== "all") {
trades = trades.filter(t => t.chamber === currentChamber);
}
trades.sort((a, b) => {
let valA = a[currentSort.key] || "";
let valB = b[currentSort.key] || "";
if (currentSort.key === "disclosure_date" || currentSort.key === "transaction_date") {
valA = new Date(valA) || 0;
valB = new Date(valB) || 0;
}
if (valA < valB) return currentSort.ascending ? -1 : 1;
if (valA > valB) return currentSort.ascending ? 1 : -1;
return 0;
});
const tbody = document.getElementById("trades-body");
tbody.innerHTML = trades.map(trade => {
const typeClass = trade.type.toLowerCase().includes("purchase") ? "purchase" : "sale";
const chamberClass = trade.chamber === "Senate" ? "chamber-senate" : "chamber-house";
return `<tr>
<td>${trade.politician}</td>
<td><span class="chamber-tag ${chamberClass}">${trade.chamber}</span></td>
<td><strong>${trade.ticker}</strong></td>
<td class="${typeClass}">${trade.type}</td>
<td>${trade.amount}</td>
<td>${trade.transaction_date}</td>
<td>${trade.disclosure_date}</td>
</tr>`;
}).join("");
}
loadTrades();
What does this JavaScript do?
- The loadTrades() function fetches trades.json from the same folder (served by Live Server) and stores the data in allTrades.
- The renderTrades() function filters by chamber, sorts by the current sort key, then builds HTML table rows using template literals.
- Each trade gets color-coded: purchases appear in green, sales in red. Chamber tags get distinct background colors.
- If trades.json does not exist, the table shows a helpful error message instead of crashing silently.
- Save index.html and check your browser.
- You should see the table populated with rows of congressional stock trades. Each row shows a politician name, a colored chamber tag, a bold ticker symbol, a green or red trade type, an amount range, and two dates.
Table shows the error message instead of data?
The error message means trades.json could not be loaded. Make sure trades.json is in the same folder as index.html. If you have not run the Python script yet, switch back to your terminal and run:
python3 fetch_trades.py
Then refresh the browser page.
Help me get trades.json loading in my browser dashboard.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html and that your browser shows a table full of trade data.
ⓧ I'd like to double check the full code
Here is the complete index.html file as it should look at the end of this step:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Congressional Stock Tracker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #1a1a2e; color: #e0e0e0; padding: 20px; }
h1 { text-align: center; margin-bottom: 10px; color: #ffffff; font-size: 1.8rem; }
.subtitle { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; }
.filters { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
.filter-btn { padding: 8px 20px; border: 1px solid #444; background-color: #2a2a3e; color: #e0e0e0; border-radius: 4px; cursor: pointer; font-size: 0.9rem; }
.filter-btn.active { background-color: #4a90d9; border-color: #4a90d9; color: #ffffff; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { background-color: #2a2a3e; padding: 12px 10px; text-align: left; cursor: pointer; user-select: none; border-bottom: 2px solid #444; }
th:hover { background-color: #3a3a4e; }
td { padding: 10px; border-bottom: 1px solid #333; }
tr:nth-child(even) { background-color: #22223a; }
tr:hover { background-color: #2a2a4e; }
.purchase { color: #4caf50; }
.sale { color: #f44336; }
.chamber-tag { padding: 2px 8px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
.chamber-senate { background-color: #1e3a5f; color: #64b5f6; }
.chamber-house { background-color: #3e2723; color: #ffab91; }
</style>
</head>
<body>
<h1>Congressional Stock Tracker</h1>
<p class="subtitle">Real financial disclosure data from the U.S. Senate and House of Representatives</p>
<div class="filters">
<button class="filter-btn active" data-chamber="all">All</button>
<button class="filter-btn" data-chamber="Senate">Senate</button>
<button class="filter-btn" data-chamber="House">House</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th data-sort="politician">Politician</th>
<th data-sort="chamber">Chamber</th>
<th data-sort="ticker">Ticker</th>
<th data-sort="type">Type</th>
<th data-sort="amount">Amount</th>
<th data-sort="transaction_date">Trade Date</th>
<th data-sort="disclosure_date">Disclosed</th>
</tr>
</thead>
<tbody id="trades-body"></tbody>
</table>
</div>
<script>
let allTrades = [];
let currentChamber = "all";
let currentSort = { key: "disclosure_date", ascending: false };
async function loadTrades() {
try {
const response = await fetch("trades.json");
allTrades = await response.json();
renderTrades();
} catch (error) {
document.getElementById("trades-body").innerHTML =
'<tr><td colspan="7" style="text-align:center;padding:20px;">Run fetch_trades.py first to generate trades.json</td></tr>';
}
}
function renderTrades() {
let trades = allTrades;
if (currentChamber !== "all") {
trades = trades.filter(t => t.chamber === currentChamber);
}
trades.sort((a, b) => {
let valA = a[currentSort.key] || "";
let valB = b[currentSort.key] || "";
if (currentSort.key === "disclosure_date" || currentSort.key === "transaction_date") {
valA = new Date(valA) || 0;
valB = new Date(valB) || 0;
}
if (valA < valB) return currentSort.ascending ? -1 : 1;
if (valA > valB) return currentSort.ascending ? 1 : -1;
return 0;
});
const tbody = document.getElementById("trades-body");
tbody.innerHTML = trades.map(trade => {
const typeClass = trade.type.toLowerCase().includes("purchase") ? "purchase" : "sale";
const chamberClass = trade.chamber === "Senate" ? "chamber-senate" : "chamber-house";
return `<tr>
<td>${trade.politician}</td>
<td><span class="chamber-tag ${chamberClass}">${trade.chamber}</span></td>
<td><strong>${trade.ticker}</strong></td>
<td class="${typeClass}">${trade.type}</td>
<td>${trade.amount}</td>
<td>${trade.transaction_date}</td>
<td>${trade.disclosure_date}</td>
</tr>`;
}).join("");
}
loadTrades();
</script>
</body>
</html>
Your dashboard is live and showing real congressional stock trades. Next up, you will wire up the filter buttons and add click-to-sort on the column headers so you can explore the data interactively.
Add Filtering and Sorting
Your dashboard already displays real congressional trade data in a styled table. That is a solid foundation.
Right now, though, the data is static. You cannot narrow the view to just Senate trades or sort by a specific column. In this step, you will wire up the filter buttons and column headers so the dashboard becomes truly interactive.
In this step, get ready to:
- Add a chamber filter that dynamically shows or hides trades by clicking All, Senate, or House.
- Add column header click sorting so you can reorder trades by date, politician name, or amount.
- Test the full pipeline end-to-end from Python script to browser.
Add the chamber filter event listeners
The filter buttons already exist in your HTML, and the renderTrades() function already knows how to filter by currentChamber. The missing piece is the event listener that connects button clicks to the filtering logic.
- In index.html, scroll to the <script> section.
- Find the line that says loadTrades(); at the very bottom of the script.
- Add the following code directly above loadTrades();:
document.querySelectorAll(".filter-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentChamber = btn.dataset.chamber;
renderTrades();
});
});
What does this code do?
This code selects every button with the class filter-btn and attaches a click handler to each one.
- When a button is clicked, it first removes the active class from all filter buttons so none appear highlighted.
- It then adds active to the clicked button so it gets the blue highlight style.
- Finally, it updates currentChamber with the button's data-chamber attribute and re-renders the table.
- Save index.html.
- Refresh your browser. Click the Senate button. You should see only Senate trades in the table, and the button turns blue.
Clicking a button does nothing?
- Make sure the new code is above loadTrades();, not below it. The event listeners must be registered before the page finishes loading.
- Open DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows) and check the Console tab for any red error messages. A typo in the selector string will prevent the listener from attaching.
Still stuck? Help me debug why my filter buttons are not responding to clicks.
Add column header click sorting
The table headers already have data-sort attributes identifying which field each column corresponds to. You will now connect those headers to the sorting logic inside renderTrades().
- In index.html, add the following code directly below the filter button listener you just added (still above loadTrades();):
document.querySelectorAll("th[data-sort]").forEach(th => {
th.addEventListener("click", () => {
const key = th.dataset.sort;
if (currentSort.key === key) {
currentSort.ascending = !currentSort.ascending;
} else {
currentSort.key = key;
currentSort.ascending = true;
}
renderTrades();
});
});
What does this code do?
This selects every <th> element that has a data-sort attribute and adds a click handler.
- If you click the same column header twice, it toggles between ascending and descending order.
- If you click a different column header, it switches to that column and starts in ascending order.
- After updating currentSort, it calls renderTrades() to re-render the table with the new sort applied.
- Save index.html.
- Refresh your browser and click the Politician column header. The table rows should reorder alphabetically by politician name.
- Click Politician again. The order should reverse.
Column sort not working?
- Confirm you placed the code between the filter listener block and loadTrades();. If it is outside the <script> tag, it will not run.
- Check that the selector string is exactly th[data-sort] with no spaces. A mismatch means no headers get listeners.
Need help? Help me figure out why clicking column headers does not sort my table.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.html before moving on.
ⓧ I'd like to double check the full code
Here is the complete index.html file for comparison:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Congressional Stock Tracker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #1a1a2e; color: #e0e0e0; padding: 20px; }
h1 { text-align: center; margin-bottom: 10px; color: #ffffff; font-size: 1.8rem; }
.subtitle { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; }
.filters { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
.filter-btn { padding: 8px 20px; border: 1px solid #444; background-color: #2a2a3e; color: #e0e0e0; border-radius: 4px; cursor: pointer; font-size: 0.9rem; }
.filter-btn.active { background-color: #4a90d9; border-color: #4a90d9; color: #ffffff; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { background-color: #2a2a3e; padding: 12px 10px; text-align: left; cursor: pointer; user-select: none; border-bottom: 2px solid #444; }
th:hover { background-color: #3a3a4e; }
td { padding: 10px; border-bottom: 1px solid #333; }
tr:nth-child(even) { background-color: #22223a; }
tr:hover { background-color: #2a2a4e; }
.purchase { color: #4caf50; }
.sale { color: #f44336; }
.chamber-tag { padding: 2px 8px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
.chamber-senate { background-color: #1e3a5f; color: #64b5f6; }
.chamber-house { background-color: #3e2723; color: #ffab91; }
</style>
</head>
<body>
<h1>Congressional Stock Tracker</h1>
<p class="subtitle">Real financial disclosure data from the U.S. Senate and House of Representatives</p>
<div class="filters">
<button class="filter-btn active" data-chamber="all">All</button>
<button class="filter-btn" data-chamber="Senate">Senate</button>
<button class="filter-btn" data-chamber="House">House</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th data-sort="politician">Politician</th>
<th data-sort="chamber">Chamber</th>
<th data-sort="ticker">Ticker</th>
<th data-sort="type">Type</th>
<th data-sort="amount">Amount</th>
<th data-sort="transaction_date">Trade Date</th>
<th data-sort="disclosure_date">Disclosed</th>
</tr>
</thead>
<tbody id="trades-body"></tbody>
</table>
</div>
<script>
let allTrades = [];
let currentChamber = "all";
let currentSort = { key: "disclosure_date", ascending: false };
async function loadTrades() {
try {
const response = await fetch("trades.json");
allTrades = await response.json();
renderTrades();
} catch (error) {
document.getElementById("trades-body").innerHTML =
'<tr><td colspan="7" style="text-align:center;padding:20px;">Run fetch_trades.py first to generate trades.json</td></tr>';
}
}
function renderTrades() {
let trades = allTrades;
if (currentChamber !== "all") {
trades = trades.filter(t => t.chamber === currentChamber);
}
trades.sort((a, b) => {
let valA = a[currentSort.key] || "";
let valB = b[currentSort.key] || "";
if (currentSort.key === "disclosure_date" || currentSort.key === "transaction_date") {
valA = new Date(valA) || 0;
valB = new Date(valB) || 0;
}
if (valA < valB) return currentSort.ascending ? -1 : 1;
if (valA > valB) return currentSort.ascending ? 1 : -1;
return 0;
});
const tbody = document.getElementById("trades-body");
tbody.innerHTML = trades.map(trade => {
const typeClass = trade.type.toLowerCase().includes("purchase") ? "purchase" : "sale";
const chamberClass = trade.chamber === "Senate" ? "chamber-senate" : "chamber-house";
return `<tr>
<td>${trade.politician}</td>
<td><span class="chamber-tag ${chamberClass}">${trade.chamber}</span></td>
<td><strong>${trade.ticker}</strong></td>
<td class="${typeClass}">${trade.type}</td>
<td>${trade.amount}</td>
<td>${trade.transaction_date}</td>
<td>${trade.disclosure_date}</td>
</tr>`;
}).join("");
}
document.querySelectorAll(".filter-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentChamber = btn.dataset.chamber;
renderTrades();
});
});
document.querySelectorAll("th[data-sort]").forEach(th => {
th.addEventListener("click", () => {
const key = th.dataset.sort;
if (currentSort.key === key) {
currentSort.ascending = !currentSort.ascending;
} else {
currentSort.key = key;
currentSort.ascending = true;
}
renderTrades();
});
});
loadTrades();
</script>
</body>
</html>
Test the full pipeline
You have all the pieces in place. Time to run the entire workflow from data fetch to browser visualization and confirm everything works together.
- Return to your terminal and generate fresh trade data by running:
python3 fetch_trades.py
- You should see output showing how many House and Senate trades were fetched, how many passed filtering, and a confirmation that data was saved to trades.json.
- In VS Code, right-click index.html in the Explorer sidebar and select Open with Live Server.
- Your browser opens with the dashboard. Confirm the table is populated with trade data.
- Click the Senate filter button. Only Senate trades should appear.
- Click the All button to bring back all trades.
- Click the Ticker column header. The table should sort alphabetically by ticker symbol.
- Click Ticker again to reverse the sort order.
What should I see?
A dark-themed table with up to 100 rows of real congressional stock trades. The filter buttons highlight blue when active and immediately update the visible rows. Column headers reorder the table on click.
Table is empty or shows an error message?
- Make sure you ran python3 fetch_trades.py and that trades.json exists in the same folder as index.html.
- You must open the page through Live Server (not by double-clicking the file). The fetch() call requires an HTTP server to work.
- If the Python script printed an error about fetching data, check your internet connection and try running it again.
Still stuck? Help me debug why my congressional stock tracker dashboard is not showing any trade data.
Your congressional stock tracker is fully interactive. You can filter by chamber and sort by any column to explore the data. Next up, you will tackle a bonus challenge that adds real-time search to the dashboard.
Secret mission
Add a Search Bar
Your dashboard has filtering and sorting, but what if you want to quickly find trades by a specific politician or stock ticker? In this mission, you'll add a real-time search bar that narrows results as you type.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder on your Desktop containing fetch_trades.py, trades.json, and index.html.
- Python virtual environment (inside your project folder).
- Live Server extension installed in VS Code.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment with the dashboard further.
- Your project folder stays on your Desktop.
- Live Server remains installed in VS Code for future projects.
- You can re-run fetch_trades.py any time to refresh the trade data.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files stay on disk so you can pick back up later.
- If Live Server is running, click Go Live in the VS Code status bar to stop it.
- Close the browser tab showing your dashboard.
- Your project folder and virtual environment remain untouched for next time.
Delete - I don't want to use this again
Remove all project files and start fresh. This cannot be undone.
- If Live Server is running, click Go Live in the VS Code status bar to stop it.
- Delete your project folder (which includes the virtual environment and all project files) by running this command in your terminal:
macOS
rm -rf ~/Desktop/congressional-stock-tracker
Windows
rmdir /s /q %USERPROFILE%\Desktop\congressional-stock-tracker
- To remove the Live Server extension, open the Extensions panel in VS Code, find Live Server, and click Uninstall.
That's a Wrap!
That's a Wrap!
Nice work! You just built a complete data pipeline that pulls real congressional stock trades from public government datasets and displays them in a professional web dashboard.
You've learned how to:
- Fetch and normalize data from two different public sources (Senate and House) into a unified format using Python and the requests library.
- Process, filter, and export data to JSON, then build an HTML/JavaScript dashboard that renders the data dynamically in the browser.
- Add interactive chamber filtering and column sorting to a web table using vanilla JavaScript event listeners.
- Secret Mission: You added real-time search functionality that filters trades by politician name or ticker symbol.
Ready to quiz yourself?