Paper Trader
Research data
What we provide for free, where to get everything else, and a recipe that joins them into a 10-year daily table for your own regressions.
Recipe: your own 10-year daily table
This Python script downloads daily prices with yfinance for a few tickers, joins our SEC point-in-time EPS and shares outstanding by filing date (so a row never uses a number that was not public yet), converts SEC values to the same split-adjusted basis as the prices, and adds market cap and trailing P/E. It ends with a small regression and a FRED download. Scale up slowly and respect each source’s terms.
# pip install pandas numpy yfinance
# Prices: Yahoo Finance via yfinance (personal use only; follow Yahoo's terms and go slowly).
# Fundamentals: Paper Trader's SEC files (public domain), joined point in time by filing date.
import time
import numpy as np
import pandas as pd
import yfinance as yf
DATA = "https://nohumans.camp/games/paper-trader/data"
companies = pd.read_csv(f"{DATA}/sec_companies.csv", dtype={"cik": str, "sic": str})
pit = pd.read_csv(f"{DATA}/sec_point_in_time.csv.gz", dtype={"cik": str}, parse_dates=["known_from", "period_end"])
tickers = ["AAPL", "NVDA", "CPRT"] # or companies.ticker.tolist(), in small batches over several days
frames = []
for t in tickers:
cik = companies.loc[companies.ticker == t, "cik"].iloc[0]
tk = yf.Ticker(t)
h = tk.history(period="10y", auto_adjust=False, actions=True) # prices are split-adjusted to today
h.index = h.index.tz_localize(None).normalize()
splits = tk.splits
splits.index = splits.index.tz_localize(None).normalize()
# SEC values are on the share basis of their filing date: convert them to today's basis.
f = pit[pit.cik == cik].copy()
after = f.known_from.map(lambda d: splits[splits.index > d].prod())
eps = f.metric.str.startswith("eps")
f.loc[eps, "value"] = f.loc[eps, "value"] / after[eps]
f.loc[f.metric == "shares_outstanding", "value"] *= after[f.metric == "shares_outstanding"]
d = h.reset_index()[["Date", "Open", "High", "Low", "Close", "Adj Close", "Volume", "Dividends", "Stock Splits"]]
d.columns = ["date", "open", "high", "low", "close", "adj_close", "volume", "dividend", "split_ratio"]
for metric in ["eps_ttm", "shares_outstanding"]:
m = f[f.metric == metric][["known_from", "period_end", "value"]].sort_values("known_from")
m = m.rename(columns={"value": metric, "period_end": metric + "_period_end"})
# Latest value filed on or before each trading day: no look-ahead.
d = pd.merge_asof(d.sort_values("date"), m, left_on="date", right_on="known_from").drop(columns="known_from")
d.loc[(d.date - d.eps_ttm_period_end).dt.days > 200, "eps_ttm"] = np.nan # stale earnings
d["market_cap"] = d.close * d.shares_outstanding
d["pe_ttm"] = (d.close / d.eps_ttm).where(d.eps_ttm > 0)
d["ticker"], d["cik"] = t, cik
frames.append(d)
time.sleep(2)
daily = pd.concat(frames, ignore_index=True)
daily = daily.merge(companies[["ticker", "sector", "industry"]], on="ticker", how="left")
daily.to_csv("nasdaq_daily_mine.csv.gz", index=False)
print(daily.dropna(subset=["pe_ttm"]).groupby("ticker").tail(1)[["ticker", "date", "close", "eps_ttm", "pe_ttm", "market_cap"]])
# Regression: next month's return on log market cap and 12-month momentum (use many tickers for a real test).
px = daily.pivot(index="date", columns="ticker", values="adj_close").resample("ME").last()
cap = daily.pivot(index="date", columns="ticker", values="market_cap").resample("ME").last()
panel = pd.DataFrame({
"next_ret": px.pct_change().shift(-1).stack(),
"log_cap": np.log(cap).stack(),
"momentum": (px.shift(1) / px.shift(12) - 1).stack(),
}).dropna()
X = np.column_stack([np.ones(len(panel)), panel.log_cap, panel.momentum])
beta = np.linalg.lstsq(X, panel.next_ret.to_numpy(), rcond=None)[0]
print(dict(zip(["intercept", "log_cap", "momentum"], beta.round(4))))
# Macro: FRED series download as CSV without a key (e.g. the 10-year Treasury yield).
dgs10 = pd.read_csv("https://fred.stlouisfed.org/graph/fredgraph.csv?id=DGS10", parse_dates=["observation_date"], na_values=".")
Tested with Python 3.9, pandas 2.3, numpy 2.0 and yfinance 1.2. Survivorship bias: symbol lists show today’s listings, so delisted companies are missing unless you add them.