Download US IPO data from LSEG Workspace in Python

LSEG’s documentation gives no script that pulls the SDC New-Issues database and cleans it down to the set of initial public offerings (IPOs) that academic studies use, so I wrote one. The script returns 8,175 operating-company IPOs between 1988 and 2025, delisted and merged issuers included.
The script runs in five steps. I pull the deals by issue date, keyed to a permanent SDC package number, so companies that later delisted or merged stay in the sample, and the pull is survivorship-free. Then I filter the raw deals to major-exchange common stock and check the count against Jay Ritter’s public series.
One caveat before we start. The deals database requires a separate LSEG entitlement called DEALSEQ. If your account does not have DEALSEQ, Step 1 returns zero rows, and you fall back to Ritter’s free IPOALL.xlsx. I give the fallback at the end.
What you end up with
- Every US IPO since 1988, active, delisted, and merged
- One row per deal, keyed to its SDC package number
- Enrichment columns: issuer, exchange, security type, offer price, proceeds, and flags for blank-check companies (SPACs, shells set up to buy another firm) and business-development companies (BDCs, funds that invest in small private firms)
- A cleaned operating-company universe: major exchange, common stock, offer price at least $5, SPACs and BDCs excluded
- A monthly IPO count whose annual totals correlate with Ritter’s NET series at 0.98
- The S&P 500 total-return series, dividends reinvested, as a benchmark
Step 1. Opening a session and testing access
The deals screens, LSEG’s queries of the deals database, are large, so raise the HTTP timeout before the session opens. The default is a few seconds, and the screens time out with the default.
import warnings
# This runs before the other imports on purpose. lseg.data and pandas can raise a warning
# while they are themselves importing, and a filter set afterwards would come too late.
warnings.filterwarnings("ignore") # suppress the lseg and pandas warnings
import numpy as np
import pandas as pd
import lseg.data as ld
ld.get_config().set_param("http.request-timeout", 300) # must precede open_session
ld.open_session()Run one recent year first. If the screen returns rows, your account has DEALSEQ. If the screen returns an access error, your account does not have DEALSEQ, so use Ritter instead.
Step 2. Pulling every US IPO deal, year by year
The universe is IN(DEALS) with the DEALSEQ package hint. The old DEALSECM token returns zero rows, so avoid it. The TR.NI* fields are the New-Issues set: issue date, issuer, package number, security type, exchange, offer price, proceeds, and the SPAC and BDC flags.
ISSUER_NATION = "US"
# The SDC deals screen: every US IPO in [a, b], with dates as YYYYMMDD.
# Universe is IN(DEALS) with the DEALSEQ package hint. TR.NIIssueDate gates the window.
def deals_screen(a, b):
return ("SCREEN(U(IN(DEALS)/*UNV:DEALSEQ*/),"
f'IN(TR.NIIssuerNation,"{ISSUER_NATION}"),'
"TR.NIIsECM=true,TR.NIIsIPO=true,"
f"BETWEEN(TR.NIIssueDate,{a},{b}))")
# Enrichment fields, in the order get_data returns them.
DEAL_FIELDS = [
"TR.NIIssueDate", # the deal date, used for the monthly count
"TR.NIIssuer",
"TR.NISdcPackageNumber", # SDC's permanent key for the deal
"TR.NIIssueType",
"TR.NIIssueDesc", # the security type, used by the filter below
"TR.NIListingNation",
"TR.NIProceedsAmtAllMkts",
"TR.NIOfferPrice",
"TR.NIIsBlankCheckCompany", # the SPAC flag
"TR.NIIsBusinessDevelopmentCompany",
"TR.NIIssuerPrimarySic",
]The screen keys on issuer nation. Swap the country code in ISSUER_NATION to pull another country’s IPOs. The clean-up in Step 4 is US-specific. MAJOR_EXCHANGES lists US exchanges and the Ritter cross-check counts US IPOs, so another country needs its own exchange list and its own benchmark count.
Pull one year at a time. A one-year window of issue dates is selective, so each screen is small and stays under LSEG’s cap on rows per request. I set the currency parameter, Curn, to USD, which applies to the money fields, the offer price and the proceeds.
frames = []
for yr in range(1988, 2027): # SDC US IPO coverage is solid from 1988
df = ld.get_data(deals_screen(f"{yr}0101", f"{yr}1231"),
DEAL_FIELDS, parameters={"Curn": "USD"})
if df is not None and len(df):
frames.append(df)
deals = pd.concat(frames, ignore_index=True)
print(f"raw deal rows pulled: {len(deals):,}")The raw pull returns more than 17,000 deal rows across the 39 years. That number is before any cleaning, so the raw pull still includes units, American depositary receipts, master limited partnerships, closed-end funds, and off-exchange listings.
Step 3. Dropping duplicates on the SDC package number
One IPO can appear on several rows, for example a US and an international part, or tranche, of the same deal. The SDC package number is the permanent key for the deal, so dropping duplicates on it gives one row per IPO.
COLS = ["instrument","issue_date","issuer","pkg","issue_type","issue_desc","listing",
"proceeds","offer_price","is_spac","is_bdc","sic"]
# get_data returns the columns in the order the fields were asked for, plus the instrument
# in front, so they can be named by position. Slicing COLS lets the assignment succeed if a
# trailing field came back empty and was dropped; a field dropped from the middle would
# shift every name after it, so check the column count if the field list changes.
deals.columns = COLS[:deals.shape[1]]
deals["issue_date"] = pd.to_datetime(deals["issue_date"], errors="coerce")
deals = deals.dropna(subset=["issue_date"])
# The package number is SDC's permanent key for a deal, so one row per package is one row
# per IPO even when the same deal was filed as several tranches.
deals = deals.drop_duplicates("pkg")Step 4. Cleaning to the operating-company universe
Ritter’s NET series keeps major-exchange common stock with an offer price of at least $5, and drops SPACs and BDCs. TR.NIListingNation and TR.NIIssueDesc cannot be used as conditions inside the screen, so I filter on them here in pandas.
MAJOR_EXCHANGES = {"Nasdaq","New York Stock Exchange","American","NYSE Amex","NYSE MKT","NYSE Arca"}
COMMON_TYPES = {"Common Shares","Class A Shares","Ord/Common Shs.","Ordinary Shares",
"Class A Ord Shs","Class B Shares"}
d = deals.copy()
# The description arrives with the share count in front, as in "12,500,000 Common Shares",
# so the leading digits, commas and dots are stripped to leave the security type alone.
d["desc_clean"] = (d["issue_desc"].astype(str)
.str.replace(r"^[\d,\.]+\s*", "", regex=True)
.str.strip())
# SDC writes -999 where it has no offer price. Make it missing so the sentinel never
# enters a later calculation on the price. The $5 test drops the row either way.
d["offer_price"] = pd.to_numeric(d["offer_price"], errors="coerce").replace(-999, np.nan)
# One condition per line: major exchange, common stock, at least $5, no SPAC, no BDC.
# Each is a column of True and False, and & keeps only the rows that pass all five.
on_major_exchange = d["listing"].isin(MAJOR_EXCHANGES)
is_common_stock = d["desc_clean"].isin(COMMON_TYPES)
priced_at_five = d["offer_price"] >= 5.0
not_a_spac = d["is_spac"] != True
not_a_bdc = d["is_bdc"] != True
keep = on_major_exchange & is_common_stock & priced_at_five & not_a_spac & not_a_bdc
clean = d[keep].copy()
# The monthly IPO count. resample("ME") groups the rows by month end and size() counts each month.
cnt = clean.set_index("issue_date").resample("ME").size().rename("n_ipos")
print(f"clean operating-company IPOs: {len(clean):,} (of {len(d):,} raw)")Remove not_a_spac from keep to retain the SPACs. In this pull the 2021 count is about 330 with the blank-check filter and about 1,100 without it. I drop the blank-check companies by default, so a study of IPO booms does not depend on the 2021 blank-check deals.
Step 5. Pulling the S&P 500 total-return benchmark
I compare the monthly IPO count to the market in the IPO-boom study linked below, so pull the S&P 500 total-return series alongside the counts. The S&P 500 total-return series starts in 1988, the same year the deals coverage does.
today = str(pd.Timestamp.now().date())
history = ld.get_history(universe=".SPXTR", fields=["TRDPRC_1"],
start="1986-12-31", end=today)
sp = history.copy()
sp.columns = ["spxtr"] # one field was asked for, so there is one column
sp.index.name = "date"
# A weekend or holiday comes back as a row with no price. Keeping only the rows that hold
# a real number leaves the trading days.
sp = sp[pd.to_numeric(sp["spxtr"], errors="coerce").notna()]Validating against Ritter’s series
Jay Ritter of the University of Florida publishes an annual IPO count, and his NET column is the operating-company definition this pipeline reproduces. From 1988 to 2025, the cleaned LSEG universe contains 8,175 IPOs and Ritter’s NET series 7,507, and their annual totals have a correlation of 0.98, where 1 would mean that the two move in perfect step. So the cleaned pull reproduces the annual pattern of Ritter’s series, with deal-level records that include delisted issuers.
Download IPOALL.xlsx from site.warrington.ufl.edu/ritter to cross-check. The GROSS column counts everything including SPACs, and the NET column is the one this pipeline matches.
What you need
- An LSEG Workspace licence with the separate DEALSEQ (SDC deals) entitlement
- The
lseg.dataPython API, bundled with Workspace - The HTTP timeout raised before the session opens, or the large screens fail
Without DEALSEQ, the deals screen returns zero rows. In that case, download Ritter’s free IPOALL.xlsx and read the NET column. Ritter’s file gives you the annual count this pipeline reproduces, without the deal-level detail, the monthly count, or the delisted issuers.
Conclusion
The takeaway is that the SDC deals database, cleaned to operating-company IPOs, gives 8,175 US IPOs from 1988 to 2025, delisted issuers included, and reproduces the annual pattern of Ritter’s counts with a correlation of 0.98.
Read next
- Does an IPO boom precede a weak year for the S&P 500? A timing study built on the monthly IPO counts from this pipeline.
- The priciest profitable IPOs pop 13.2% and then return -2.1% over their first year Whether IPO size and valuation predict IPO returns in this data.
Disclaimer: this is a data-pipeline walkthrough for discussion purposes only. Not investment advice.
Data: US IPO deal records from LSEG’s SDC New-Issues database and the S&P 500 total-return index from LSEG Workspace, both licensed. Because the data is licensed, I cannot share the raw file.