crsp-lseg-splice

SkillDev tools

Use when "CRSP is stale / out of date", "CRSP only goes through December", "my panel ends in December", "why do my returns stop", "extend CRSP to today", "fill forward CRSP with LSEG", "current stock prices for my CRSP panel", "up-to-date returns for permnos", "splice CRSP and LSEG", "CUSIP to RIC for a CRSP universe", "backfill the CRSP gap", or any request to carry a CRSP daily/monthly stock series past CRSP''s last data date using LSEG/Refinitiv. Use proactively whenever a CRSP panel has to reach a date after CRSP''s annual cutoff, even if the user never mentions LSEG, CRSP returns a short panel without erroring, and a naive concat fabricates the seam return. NEGATIVE ROUTING: a CRSP query that stays inside CRSP''s coverage, or CIZ table and column names, is crsp-v2; LSEG session, quota, entitlement or general symbology questions are lseg-data; raw WRDS PostgreSQL access and connection patterns are wrds, this skill covers only the join between CRSP and LSEG.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the crsp-lseg-splice skill

What this skill tells your AI

The instructions your AI receives, as published by edwinhu/workflows in skills/crsp-lseg-splice/SKILL.md and read by ahel’s review.

What this skill carries — grep references/ for any subject the names below miss: !d=${CLAUDE_SKILL_DIR}; command -v skill-toc >/dev/null 2>&1 && exec skill-toc "$d"; s=$HOME/.claude/skills/plugin-utils/bin/skill-toc; [ -x "$s" ] && exec "$s" "$d"; echo "(skill-toc unavailable: references and scripts are NOT listed here — install the plugin-utils plugin, or start a new session so its bin/ reaches PATH)"

Contents

  • The Problem
  • Splice Enforcement
  • Symbology Enforcement
  • Measured Coverage
  • The Pipeline
  • Additional Resources

The Problem

CRSP ships annually. The CIZ daily file stops at the last December close and does not error when you query past it — it returns a short panel. LSEG is current to T-1 but is keyed on RIC, not PERMNO, so the join runs through CUSIP.

Verified 2026-07-28 on WRDS PostgreSQL:

SourceLast dateGap
crsp.stkdlysecurityprimarydata (CIZ)2025-12-31
crsp.stkmthsecuritydata (CIZ)2025-12-31
LSEG TRDPRC_1 / TR.TotalReturn1DT-1 (2026-07-27)141 trading days

This skill assumes crsp-v2 (CIZ table and column names) and lseg-data (session, quota, entitlements). It covers only what is specific to joining them.

Splice Enforcement

IRON LAW: NO LSEG PRICE LEVEL SPLICED ONTO A CRSP PRICE LEVEL

Measured on the Dec-2025 overlap, where both sources have the same 22 trading days: 8 of 300 sampled securities disagree on price by >5%, at a ratio that is constant to std = 0.0000 across all 22 days — the signature of an adjustment factor, not a data error. STRO.O ratio 0.1000 (1:10 reverse split), VISN.O ratio 2.0493.

  • pd.concat([crsp_prc, lseg_prc])WRONG. A 1:10 reverse split after the cutoff prints a +900% one-day return on the seam.
  • Chain the gap off CRSP's last DlyPrc with LSEG's daily total returns → CORRECT. p_t = p_cutoff * cumprod(1 + ret).

A fabricated ±90% return on a known date, in a panel that otherwise validates, is worse than a missing tail — it survives every summary statistic and lands in the event window. scripts/crsp_lseg_splice.py::rebuild_price() does the chaining.

The chained series is a total-return index anchored at CRSP's last price, not a quoted price: total-return chaining reinvests dividends into the level. The raw LSEG quote is carried alongside as lseg_prc for anyone who needs the level.

Return Facts

  • TR.TotalReturn1D is the DlyRet analogue and it validates. On the Dec-2025 overlap, 99.12% of permno-days agree to within 1e-5, median absolute difference 2.45e-07, correlation 0.991. This is the field to splice on.
  • It is returned in PERCENT. -6.369427 means −6.37%. CRSP DlyRet is a decimal. Divide by 100 at the boundary or every return in the gap is off by 100×, which is loud in a mean but silent inside a signal that gets standardized.
  • It is CALENDAR-PADDED, and that breaks any coverage count taken from it. Every instrument comes back with the identical number of return days — measured std = 0.0000 across 3,384 RICs — including the 19 that delisted mid-gap. 1.14% of return rows have no trade price that day and 38% of those are ret == 0. Build the panel by inner-joining returns against the price series: otherwise a delisted stock keeps emitting flat rows to the end of the window, and the fill rate reads 99.8% because every security looks complete.
  • It is dividend-inclusive, like CIZ DlyRet, so no distribution merge is needed — and, per crsp-v2, no delisting-return merge either.
  • DlyCap must scale on the PRICE relative, not the return chain. Chaining market cap on TR.TotalReturn1D reinvests every dividend into shares outstanding and inflates cap by the cumulative dividend yield over the gap. lseg_prc_t / lseg_prc_cutoff is the right multiplier: both legs sit on LSEG's single adjusted basis, so splits cancel (as they must for cap, which is split-invariant) and dividends are excluded. TR.CompanyMarketCap exists but is not on CRSP's DlyCap definition or units (CIZ DlyCap is $ thousands).
  • Start the LSEG pull ON the CRSP cutoff date, not the day after. That one overlapping day is the anchor. crsp_prc / lseg_prc on the same day measures each security's adjustment-basis ratio directly: ~1.0 means the two agree, anything else is a post-cutoff corporate action LSEG has back-adjusted for (or a bad link that survived the venue screen). The pipeline carries it as adj_ratio and coverage reports it — it is what makes the splice auditable per security rather than trusted in bulk.
  • LSEG serves data for still-listed instruments. A security that delists inside the gap stops on its delist date, and its RIC gets a ^-stamp (IROQ.O^C26 = delisted March 2026) — which is how you learn the delist happened, since CRSP has not published it yet. There is no delisting return from this path.

Symbology Enforcement

IRON LAW: NO RIC ACCEPTED WITHOUT A US-VENUE CHECK

Measured: 101 of 3,485 resolved RICs (2.76%) carry a non-US venue suffix — .TRE (Tradegate), .MU (Munich), .SG (Stuttgart), .TBEA, .MX, .BCU.

CRSPLSEG RICeffect
TPH TRI POINTE HOMEST86f.TREprices in EUR; CRSP/LSEG ratio 1.3294 (≈ USD/EUR)
VERO VENUS CONCEPT0RR0.MUMunich listing; 18 of 22 days differ >1pp
CIVI CIVITAS RESOURCESUS17888H1032.TREISIN-form RIC, Tradegate
  • Taking RIC straight from symbol_conversionWRONG. An FX series enters the panel as a price series.
  • Filter on the suffix before pulling → CORRECT. US venues are the bare RIC (composite) and .O .N .A .P .K .PK .OQ.

A EUR price series is plausible — right order of magnitude, right shape, moves with the stock. It fails no null check and no range check. It is only caught at the venue, which is why the check belongs before the pull, not after.

Symbology Facts

  • Use CRSP's date-effective cusip9 from crsp.stksecurityinfohist, never stksecurityinfohdr.hdrcusip. CIZ inverted the SIZ naming — CIZ cusip is the historical value and hdrcusip is the header one — so header-CUSIP code attaches a security's most-recent CUSIP to its entire history. For a fill-forward anchored at the cutoff date the two usually agree, but the same script pointed at an earlier as-of date silently mis-links.
  • Pass the 9-character CUSIP to SymbolTypes.CUSIP. CRSP CIZ carries both cusip9 and the 8-character cusip; LSEG returns 9-char. Trimming to CUSIP8 to join is fine as a post-hoc key, but resolve on the 9.
  • Unresolved CUSIPs come back absent, not wrong. Misses land as explicit nulls. The precision risk in this pipeline is the foreign venue and the entity mismatch, not the miss.
  • Entity agreement is the cheap guard: CRSP ticker vs LSEG TickerSymbol agrees for 97.31%, RIC root vs CRSP ticker for 95.06%, either for 97.56%. Most residual disagreement is a rename CRSP has recorded and LSEG reports under the current name (COMMVISN.O), not a mis-link — flag it, do not drop it.
  • symbol_conversion chunks fine at 200 symbols; the session cap is 500 requests/minute and the binding limit is get_data at 10,000 data points and get_history at 3,000 rows per request.

Measured Coverage

Full numbers, denominators, and the validation method: references/coverage.md.

Universe = the 3,657 CRSP CIZ common stocks (5-column SHRCD 10/11 equivalent, primaryexch IN ('N','A','Q')) trading on 2025-12-31. Measured 2026-07-28.

Stagecountshare of universe
CRSP common stocks at cutoff3,657100%
CUSIP9 → RIC resolved3,48595.30%
— of those, US venue3,38492.53%
— of those, entity-agreeing97.56% of resolved

Agreement on the Dec-2025 overlap (300-security sample, 6,244 permno-days):

checkresult
price exact (reldiff ≤ 1e-4)96.28%
price within 5%98.13%
daily return within 1e-599.12%
return available where CRSP has a day98.34%

The 1.87% of price rows outside 5% are the split-adjustment and foreign-venue cases above — both handled by the two Iron Laws, neither by a tolerance.

The Pipeline

scripts/crsp_lseg_splice.py — five cached steps, all runs them in order:

set -a; . $XDG_RUNTIME_DIR/agenix/lseg-credentials; set +a
export RDP_APP_KEY=$LSEG_APP_KEY RDP_USERNAME=$LSEG_USERNAME RDP_PASSWORD=$LSEG_PASSWORD
python scripts/crsp_lseg_splice.py all --out data/ --start 2020-01-01
stepdoeswrites
universeCRSP CIZ common stocks alive at max(dlycaldt) + their daily paneluniverse.parquet, crsp_panel.parquet, asof.txt
mapCUSIP9 → RIC, venue + entity screen, link_status per rowlink.parquet
pullgap-period TRDPRC_1/ACVOL_UNS + TR.TotalReturn1Dlseg_hist.parquet, lseg_ret.parquet
splicereturn-chained continuous panel, source column marks provenancepanel_spliced.parquet
coveragecoverage table + seam sanity checkstdout

Output panel is permno × date with dlyprc, dlyret, dlycap, dlyvol, source, plus RIC, lseg_prc (the raw quote) and adj_ratio on the LSEG rows. source ∈ {CRSP, LSEG}. Keep source in anything downstream — the two halves have different provenance and the LSEG half is unaudited by CRSP.

Red Flags — STOP If You're About To:

  • pd.concat a CRSP price and an LSEG price → STOP. Different adjustment bases; chain on returns (Iron Law 1).
  • Use a RIC without checking its suffix → STOP. 4.3% are foreign-currency venues (Iron Law 2).
  • Use TR.TotalReturn1D without /100 → STOP. It is percent; CRSP is decimal.
  • Join on hdrcusip → STOP. Header CUSIP, not date-effective.
  • Query crsp.dsf / crsp.msf for the recent panel → STOP. Legacy SIZ, frozen at 2024-12-31 — a year before the CIZ cutoff. See crsp-v2.
  • Open a second LSEG session while a pull runs → STOP. One concurrent platform session; the second fails on quota rather than queueing.
  • Count coverage from the return series → STOP. TR.TotalReturn1D is calendar-padded; every security looks complete. Count from the price series.
  • Report the panel as "CRSP data through today" → STOP. It is CRSP through the cutoff and LSEG after, at 91% of the universe. Say so.

Additional Resources

  • references/coverage.md — full measured coverage, per-failure-mode breakdown, validation method, and the reproduction commands
  • scripts/crsp_lseg_splice.py — the pipeline
  • ${CLAUDE_SKILL_DIR}/../crsp-v2/SKILL.md — CIZ tables, the 5-column universe filter, delisting returns, CUSIP inversion
  • ${CLAUDE_SKILL_DIR}/../lseg-data/SKILL.md — session setup, quota, entitlements, rate limits
  • ${CLAUDE_SKILL_DIR}/../lseg-data/references/symbology.mdSymbolTypes enum names and the conversion API
  • ${CLAUDE_SKILL_DIR}/../wrds/SKILL.md — WRDS Postgres connection and .pgpass

Signals

GitHub stars
21
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
crsp-lseg-splice
Source
github.com/edwinhu/workflows