We check for new free financial data every Wednesday.
Free Data Recipes
Short, tested Python examples that pull free data from sources listed on Signal Lab. Every snippet was run, every limit comes from the provider, and none of it is investment advice.
Looking for copy-paste prompts for AI agents instead? See Agent Recipes.
How these were tested
Each snippet was run on 2026-10-24 with Python 3.13, pandas 3.0.6 and requests 2.34.2. The sample output is from that run; providers update, so your numbers will differ. We re-run recipes when we re-verify the source.
Pull U.S. federal debt and the average interest rate on it, with no key
Two keyless calls to the Treasury Fiscal Data API give you a daily total-debt series and a monthly average-interest-rate series as pandas DataFrames.
No key, no signup. Python 3 with requests and pandas.
Python
"""Recipe 1: U.S. federal debt (daily) and average interest rate on it (monthly), Treasury Fiscal Data API. No key."""
import requests
import pandas as pd
BASE = "https://api.fiscaldata.treasury.gov/services/api/fiscal_service"
# Debt to the Penny: total public debt outstanding, one row per business day.
debt = requests.get(
f"{BASE}/v2/accounting/od/debt_to_penny",
params={
"fields": "record_date,tot_pub_debt_out_amt",
"filter": "record_date:gte:2026-01-01",
"sort": "record_date",
"page[size]": 500, # default page size is 100 rows
},
timeout=30,
)
debt.raise_for_status()
d = pd.DataFrame(debt.json()["data"])
d["record_date"] = pd.to_datetime(d["record_date"])
d["tot_pub_debt_out_amt"] = d["tot_pub_debt_out_amt"].astype(float)
d = d.set_index("record_date")
d = d.rename(columns={"tot_pub_debt_out_amt": "total_public_debt_usd"})
print(d.tail(3).to_string(float_format="{:,.0f}".format))
print("change from first 2026 row to latest (USD):", f"{d.iloc[-1, 0] - d.iloc[0, 0]:,.0f}")
# Average interest rates on outstanding Treasury securities, monthly.
rates = requests.get(
f"{BASE}/v2/accounting/od/avg_interest_rates",
params={
"fields": "record_date,security_desc,avg_interest_rate_amt",
"filter": "security_desc:eq:Total Marketable,record_date:gte:2026-01-01",
"sort": "record_date",
"page[size]": 100,
},
timeout=30,
)
rates.raise_for_status()
r = pd.DataFrame(rates.json()["data"])
print(r.tail(3).to_string(index=False))
print("meta:", rates.json()["meta"]["count"], "rows;", rates.json()["meta"]["total-count"], "total")
Expected output
Copied from the real run; dates and values will move as the providers update.
total_public_debt_usd
record_date
2026-10-20 40,439,546,016,039
2026-10-21 40,413,639,012,306
2026-10-22 40,444,342,709,389
change from first 2026 row to latest (USD): 2,021,524,138,782
record_date security_desc avg_interest_rate_amt
2026-07-31 Total Marketable 3.443
2026-08-31 Total Marketable 3.475
2026-09-30 Total Marketable 3.518
meta: 9 rows; 9 total
https://fiscaldata.treasury.gov/api-documentation/"The data is offered free, without restriction, and available to copy, adapt, redistribute, or otherwise use for non-commercial or commercial purposes."
https://fiscaldata.treasury.gov/api-documentation/"When no page number or page size parameter is specified, the default response is Page number: 1 Page size: 100" (the snippet sets page[size] itself; the maximum page size is Not stated).
Rate limit: Not stated (nothing on the API docs page about a numeric limit; the 2026-10-22 check of the FAQ and Open Data Policy found none either).
Delay: end of each business day for the debt series; monthly for the interest-rate series.
Coverage and meaning: the average interest rate is the average rate on outstanding Treasury securities, so it is NOT a market yield and NOT today's auction rate. For yields use a different source.
License: the provider states the data is free "without restriction" for non-commercial or commercial use. "Public domain" is not stated, and no attribution requirement is stated. Say "Source: U.S. Treasury, Fiscal Data" anyway; it is accurate.
Snippet caveat: values arrive as strings and are converted with astype(float); the snippet filters to security_desc "Total Marketable", one of several security types in that dataset.
Pull the same daily USD/CAD series from the Bank of Canada Valet API and from Frankfurter, line them up and see exactly where and by how much they differ.
No key, no signup. Python 3 with requests and pandas.
Python
"""Recipe 2: the same USD/CAD series from two keyless sources, Bank of Canada Valet vs Frankfurter."""
import requests
import pandas as pd
START = "2026-09-01"
# Source A: Bank of Canada Valet, daily average USD/CAD (Canadian dollars per US dollar).
a = requests.get(
"https://www.bankofcanada.ca/valet/observations/FXUSDCAD/json",
params={"start_date": START},
timeout=30,
)
a.raise_for_status()
boc = pd.Series(
{o["d"]: float(o["FXUSDCAD"]["v"]) for o in a.json()["observations"] if o["FXUSDCAD"].get("v")},
name="boc_valet",
)
def frankfurter(name, **extra):
r = requests.get(
"https://api.frankfurter.dev/v2/rates",
params={"from": START, "base": "USD", "quotes": "CAD", **extra},
timeout=30,
)
r.raise_for_status()
return pd.Series({x["date"]: x["rate"] for x in r.json()}, name=name)
blend = frankfurter("frankfurter_blend") # default: Frankfurter's blend of sources
pinned = frankfurter("frankfurter_boc", providers="BOC") # one provider pinned: Bank of Canada
df = pd.concat([boc, blend, pinned], axis=1)
both = df.dropna()
both = both.assign(blend_minus_boc=(both["frankfurter_blend"] - both["boc_valet"]).round(4))
print(both.tail(4))
print("rows: valet", boc.size, "| blend", blend.size, "| pinned BOC", pinned.size, "| compared", len(both))
print("blend vs Valet: max abs diff", both["blend_minus_boc"].abs().max(),
"| mean abs diff", round(both["blend_minus_boc"].abs().mean(), 5))
print("pinned BOC identical to Valet on all shared dates:", bool((both["frankfurter_boc"] == both["boc_valet"]).all()))
print("dates only in the Frankfurter blend (first 5):", sorted(set(blend.index) - set(boc.index))[:5])
Expected output
Copied from the real run; dates and values will move as the providers update.
boc_valet frankfurter_blend frankfurter_boc blend_minus_boc
2026-10-20 1.4054 1.4065 1.4054 0.0011
2026-10-21 1.4057 1.4045 1.4057 -0.0012
2026-10-22 1.4021 1.4034 1.4021 0.0013
2026-10-23 1.3993 1.4014 1.3993 0.0021
rows: valet 36 | blend 54 | pinned BOC 36 | compared 36
blend vs Valet: max abs diff 0.0047 | mean abs diff 0.00251
pinned BOC identical to Valet on all shared dates: True
dates only in the Frankfurter blend (first 5): ['2026-09-05', '2026-09-06', '2026-09-07', '2026-09-12', '2026-09-13']
Sources
https://www.bankofcanada.ca/valet-api-how-to/"No registration is required to use the Valet API. You do not need an access key." / "There is no cost associated with using the Valet API."
https://www.bankofcanada.ca/valet-api-how-to/"Use caching when possible: For data that updates only once a day, such as exchange rates, store the response instead of calling the API again."
https://www.bankofcanada.ca/terms/"the Bank permits you to freely use, copy, distribute and transmit its website content under the following terms:" / "You must attribute the Bank of Canada as the source of the content, and indicate if changes were made."
https://www.bankofcanada.ca/terms/"Bank of Canada exchange rates are released for statistical and analytical purposes only and are not intended to be used as the benchmark rate for executing foreign exchange trades."
https://frankfurter.dev/"Frankfurter is a free exchange rates API for 223 currencies from 105 central banks and official sources. Rates update daily and go back to 1948. ... You don't need an API key."
https://frankfurter.dev/"By default, rates are blended across all providers. Use providers to get rates from a specific provider"; "There are no quotas. Requests are rate-limited to prevent abuse, but there are no monthly or daily caps."; "It isn't built for live trading".
https://frankfurter.dev/license/"Frankfurter doesn't own the rates, so it can't give you rights the provider doesn't. If a provider publishes terms of use, they apply." / "Rates can be late, missing, or revised."
Limits
Rate limit: Bank of Canada Not stated (the how-to points to the terms; the terms bar circumventing request limits). Frankfurter: no quotas, "rate-limited to prevent abuse", numeric limit Not stated.
Not for execution: the Bank of Canada rates are indicative daily averages for statistical use, and Frankfurter says it is not built for live trading.
Observed in this run (not a provider claim): the Frankfurter blend has rows for weekends and some holidays that the Valet series does not, and the blend differs from the Valet value on shared dates; pinning providers=BOC matched Valet on every shared date. Why the blend differs is Not stated here.
Attribution: Bank of Canada data needs attribution and a note if you changed it. Frankfurter's blend is its own calculation and free to use; provider routes follow each provider's terms.
Valet's daily FX series starts 2017-01-03 (live check 2026-10-20); earlier rates are in a legacy noon-rate series not used here.
Read a company's annual revenue and 10-K filing history from SEC EDGAR
Two calls to the SEC's data.sec.gov APIs list a company's latest 10-K filings and extract its reported annual revenue from the XBRL "company concept" feed.
No key, no signup. SEC asks automated tools to declare a User-Agent header with your name and contact email; the snippet shows a placeholder, so use your own. Python 3 with requests and pandas.
Python
"""Recipe 3: a company's annual revenue and recent 10-K filings from SEC EDGAR. No key; declare your own User-Agent."""
import requests
import pandas as pd
# SEC asks automated tools to declare a User-Agent with contact details. Replace this with your own name and email.
HEADERS = {"User-Agent": "Your Name you@example.com"}
CIK = "0000320193" # Apple Inc., zero-padded to 10 digits
# 1) Filing history (submissions API): the most recent 10-K filings.
sub = requests.get(f"https://data.sec.gov/submissions/CIK{CIK}.json", headers=HEADERS, timeout=30)
sub.raise_for_status()
recent = pd.DataFrame(sub.json()["filings"]["recent"])
tenk = recent[recent["form"] == "10-K"][["form", "filingDate", "reportDate", "accessionNumber"]]
print(sub.json()["name"])
print(tenk.head(3).to_string(index=False))
# 2) One XBRL concept over time (companyconcept API): annual revenue from 10-K filings.
con = requests.get(
f"https://data.sec.gov/api/xbrl/companyconcept/CIK{CIK}/us-gaap/RevenueFromContractWithCustomerExcludingAssessedTax.json",
headers=HEADERS,
timeout=30,
)
con.raise_for_status()
facts = pd.DataFrame(con.json()["units"]["USD"])
# keep full-year values (duration ~1 year) reported on a 10-K, one row per period end
facts["days"] = (pd.to_datetime(facts["end"]) - pd.to_datetime(facts["start"])).dt.days
annual = facts[(facts["form"] == "10-K") & facts["days"].between(350, 380)]
annual = annual.sort_values("filed").drop_duplicates("end", keep="last")
print(annual[["end", "val", "filed"]].tail(4).to_string(index=False))
Expected output
Copied from the real run; dates and values will move as the providers update.
Apple Inc.
form filingDate reportDate accessionNumber
10-K 2025-10-31 2025-09-27 0000320193-25-000079
10-K 2024-11-01 2024-09-28 0000320193-24-000123
10-K 2023-11-03 2023-09-30 0000320193-23-000106
end val filed
2022-09-24 394328000000 2024-11-01
2023-09-30 383285000000 2025-10-31
2024-09-28 391035000000 2025-10-31
2025-09-27 416161000000 2025-10-31
https://www.sec.gov/search-filings/edgar-application-programming-interfaces"The submissions API is updated with a typical processing delay of less than a second; the xbrl APIs are updated with a typical processing delay of under a minute. However these processing delays may be longer during peak filing times." / "data.sec.gov does not support Cross Origin Resource Scripting (CORS). Automated access must comply with SEC.gov's Privacy and Security Policy, as described in the Developer FAQs." (page last reviewed or updated April 8, 2025)
https://www.sec.gov/search-filings/edgar-search-assistance/accessing-edgar-data"Current max request rate: 10 requests/second." / "SEC reserves the right to limit request rates to preserve fair access for all users." / "Please declare your user agent in request headers: Sample Declared Bot Request Headers: User-Agent: Sample Company Name AdminContact@<sample company domain>.com Accept-Encoding: gzip, deflate Host: www.sec.gov"
https://www.sec.gov/about/webmaster-frequently-asked-questions"We allow scripted access to sec.gov content" / "Note that our current maximum access rate is 10 requests per second. This is carefully monitored to preserve equitable access for all users." / "Please declare your user agent in request headers"
https://www.sec.gov/privacy.htm"Current guidelines limit users to a total of no more than 10 requests per second, regardless of the number of machines used to submit requests. If a user or application submits more than 10 requests per second, further requests from the IP address(es) may be limited for a brief period."
Limits
Rate limit: SEC states a current maximum of 10 requests per second in total, regardless of the number of machines; above that, further requests "may be limited for a brief period". SEC may change this.
User-Agent: SEC's wording is "Please declare your user agent in request headers", with a sample of the form "Sample Company Name AdminContact@<sample company domain>.com". SEC's pages do not say what happens without one, and the API page does not mention it. Observed 2026-10-24 from the box: the same request returned 200 with a declared User-Agent and 403 with no User-Agent header or with python-requests' default. Treat a declared User-Agent as expected, and use your own name and email.
Delay: seconds to about a minute after dissemination, longer at peak filing times (provider statement above).
Coverage: XBRL financial data since 2009. Revenue is reported under different XBRL tags in different companies and years (this snippet uses one tag and returns nothing for a company that uses another), and old values can be restated, so each value is "as reported in the latest 10-K that contains it".
License / reuse terms for the filings data: Not stated on the API page and not checked for this recipe. Link to SEC's own pages and do not describe a license.
No CORS: call it from a server or script, not from browser JavaScript.
https://datacatalog.worldbank.org/public-licenses"The Creative Commons Attribution 4.0 International license allows users to copy, modify and distribute data in any format for any purpose, including commercial use. Users are only obligated to give appropriate credit (attribution) and indicate if they have made any changes"; "Many datasets are available under other licenses. They are labeled accordingly".
Default output is XML (we saw XML with no format parameter on 2026-10-24); pass format=json. Default page size is 50 rows; the snippet sets per_page=1000 and prints meta["pages"] so you can see if there is more.
Coverage: annual data, country values are as published by WDI and can be revised; "missing values" counts empty years (0 in this run).
License: WDI is CC BY 4.0, so credit the World Bank (World Development Indicators) and say if you changed the data. Other World Bank datasets can carry other licenses, so check the label for any indicator or source you swap in.
Keep the provider's own counts if you quote any ("nearly 16,000 indicators"); do not quote live API counts.
Load weekly CFTC futures positioning (Commitments of Traders) with Socrata
One keyless call returns a year of weekly Legacy futures-only COT data for a single market; compute trader-group net positions and date each row to its release day.
https://www.cftc.gov/MarketReports/CommitmentsofTraders/index.htm"the COT reports provide a breakdown of each Tuesday's open interest for futures and options on futures markets in which 20 or more traders hold positions equal to or above the reporting levels established by the CFTC."
https://www.cftc.gov/MarketReports/CommitmentsofTraders/index.htm"Currently, we are not providing tokens to use the Public Reporting APIs. People who are using the API are generally using it successfully without using a token. As long as you are not overusing the API, you should be able to use the API without a token."
https://www.cftc.gov/WebPolicy/index.htm"Government information at the CFTC website is in the public domain. Public domain information may be freely distributed and copied, but it is requested that in any subsequent use the CFTC be given appropriate acknowledgement."
Limits
Rate limit: Not stated; the CFTC asks you not to overuse the API.
Delay: Tuesday positions, normally public the following Friday at 3:30 p.m. ET. Date each row to the release day, not the Tuesday, or an analysis will see positions before they were public.
Coverage: only markets where 20 or more traders hold reportable positions; aggregates by trader group, not individual traders. The Legacy "non-commercial" and "commercial" groups are the CFTC's categories; this recipe does not interpret them.
License: public domain per the CFTC website policy; the CFTC requests appropriate acknowledgement. No formal wording is stated.
Other report types (Disaggregated, Traders in Financial Futures) use other datasets; this recipe uses Legacy futures only.
Load the Fama/French 3 factors straight from the library's CSV zip
Download one zip from Kenneth French's Data Library, parse the monthly block into a pandas DataFrame and check which CRSP release built it, without a data-reader package.
No key, no account. Python 3 with requests and pandas.
Python
"""Recipe 6: Fama/French 3 factors (monthly) from Kenneth French's Data Library, straight from the CSV zip. No key."""
import io
import zipfile
import requests
import pandas as pd
URL = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip"
r = requests.get(URL, timeout=60)
r.raise_for_status()
with zipfile.ZipFile(io.BytesIO(r.content)) as z:
text = z.read(z.namelist()[0]).decode("latin-1")
lines = text.splitlines()
print(next(l.strip() for l in lines if "CRSP database" in l)) # which CRSP release built the file
# The file has a monthly block, then an annual block ("Annual Factors: January-December").
start = next(i for i, l in enumerate(lines) if l.strip().startswith("Mkt-RF") or ",Mkt-RF" in l)
end = next(i for i, l in enumerate(lines) if "Annual Factors" in l)
block = "\n".join(lines[start:end]).strip()
df = pd.read_csv(io.StringIO(block), index_col=0)
df.index = pd.to_datetime(df.index.astype(str).str.strip(), format="%Y%m").to_period("M")
df.columns = [c.strip() for c in df.columns]
print(df.tail(3))
print("monthly rows:", len(df), "| first:", df.index[0], "| last:", df.index[-1]) # values are percent
Expected output
Copied from the real run; dates and values will move as the providers update.
This file was created using the 202608 CRSP database.
Mkt-RF SMB HML RF
2026-06 -1.07 3.39 3.58 0.29
2026-07 -0.61 -1.92 2.11 0.33
2026-08 2.56 0.34 -3.54 0.29
monthly rows: 1202 | first: 1926-07 | last: 2026-08
Sources
https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.htmlEach file row reads "TXT CSV Details" (direct downloads, no login). The only rights statement on the page is "Copyright Eugene F. Fama and Kenneth R. French". The page has no usage, attribution, redistribution or terms statement (full-text search of the page on 2026-10-24).
https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/Data_Library/f-f_factors.html"Daily Returns: July 1, 1926 - August 31, 2026 Weekly Returns: July 2, 1926 - August 31, 2026 Monthly Returns: July 1926 - August 2026 Annual Returns: 1927 - 2025". The page points to "Fama and French, 1993, "Common Risk Factors in the Returns on Stocks and Bonds," Journal of Financial Economics, for a complete description of the factor returns" (a description pointer, not a citation requirement).
https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.htmlFrom the January 2025 release the US research returns use CRSP's new CIZ files; in the new format monthly returns are "compounded daily returns with dividends reinvested on their ex-dates" (verified on the live page 2026-10-24).
Limits
Rate limit: Not stated.
License / reuse / attribution: Not stated. The library page and the file carry only "Copyright Eugene F. Fama and Kenneth R. French" ("Copyright 2026 ..." in the CSV); neither gives a usage, attribution or redistribution statement. We are not saying the library requires anything. As our own precaution: link to the library rather than republishing its files, and name the source when you show results.
Revisions: the full history is rebuilt each month, so past values can change; re-download before comparing with an older copy. Results can also differ from papers or caches built before the January 2025 method change.
Parsing is positional: it relies on the file having a monthly block that ends at the "Annual Factors" line. If the library changes the layout, the snippet needs a fix.
The same library offers daily and weekly files (US 3 factors) and international sets; this recipe only shows the monthly US file.
Got a short, working example that uses a free data source? Email it to signal-lab@qdt.ai with the subject "Recipe: <title>" and we may publish it with your credit.
We verify everything before it goes up. We run your snippet ourselves, check each limit and license claim against the provider's own page, and may edit or decline. We publish nothing we haven't run. We never publish API keys, and we don't publish trading advice or performance claims. By submitting, you confirm you wrote the code (or have the right to share it) and that we may publish it on Signal Lab with credit to the name you give.
Email body template
Title: (one line, "Use <source> to <do something>")
Promise: (one sentence: what the reader gets)
Source(s) used: (name + link to the provider's own docs page)
Needs a key or signup?: (none / free key: say which)
Python version and packages:
Snippet: (runnable as pasted, no secrets, under ~40 lines)
Sample output from your run: (paste it, with the run date)
Limits you know of: (rate limit, delay, coverage, license/attribution; write "Not stated" if the provider doesn't say)
Link to the provider's terms or license page:
Credit as: (name or handle, optional link)