skills/lseg-data/SKILL.md
Use when "query LSEG/Refinitiv", "fundamentals or market data from LSEG", "ESG scores", "RIC/ISIN symbology", "corporate governance or activism (poison pills, campaigns)", "M&A or IPO deals", "syndicated loans or project finance", "PE/VC investments", "joint ventures", "municipal bonds", "Lipper fund details", "stock screening (fscreen)", "Refinitiv news", "Workspace web client", "Codebook", or any use of the `lseg.data` Python API. (For academic loan/PE data, WRDS DealScan/PitchBook may be the better source — the wrds skill covers those.)
npx skillsauth add edwinhu/workflows lseg-dataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Access financial data from LSEG (London Stock Exchange Group), formerly Refinitiv, via the lseg.data Python library or by driving the Workspace web client over CDP.
Pick the lowest-numbered path that can serve the request.
| # | Path | Use for | Auth | Reference |
|---|------|---------|------|-----------|
| 1 | lseg.data Python library | anything it covers; batch and production work | RDP machine credentials | this file + references/* |
| 2 | Token-lift → RDP REST from Python | the same data with no machine credentials — borrows the browser session | Workspace tab's edp-token | references/workspace-web-cdp.md |
| 3 | In-page fetch() on the target origin | Workspace-internal endpoints only (SDC deal universes, FSCREEN) | browser cookies | references/workspace-web-cdp.md |
Path 1 remains preferred where it works — pip install lseg-data is available on every platform. Paths 2 and 3 exist because some data is only reachable through the web client, and because the token-lift avoids needing machine credentials at all.
The desktop Workspace app is Windows/macOS only. On Linux there is no desktop session and no Electron binary to launch with --remote-debugging-port; the web client at https://workspace.refinitiv.com/web is the only Workspace surface. Never emit a session.desktop.workspace config or an app path on Linux.
The helper for paths 2 and 3 is scripts/workspace_cdp.py:
python3 scripts/workspace_cdp.py token # verify the browser session
python3 scripts/workspace_cdp.py datagrid --universe AAPL.O,MSFT.O \
--fields TR.CommonName,TR.Revenue
import sys; sys.path.insert(0, "scripts")
import workspace_cdp as w
df = w.datagrid_df(["AAPL.O"], ["TR.Revenue", "TR.Revenue.fperiod"],
{"SDate": "0", "EDate": "-4", "Frq": "FY"})
Requires Chromium on CDP port 9222 with a signed-in Workspace Web tab — see the browser-automation skill for the browser, and references/workspace-web-cdp.md for session setup.
Before claiming ANY LSEG query succeeded, follow these steps:
.head() or .sample()This is not negotiable. Skipping result inspection is NOT HELPFUL — the user builds analysis on data with undetected quality problems.
.O, .N, .L, .T) before querying.get_data() 10,000 data points, get_history() 3,000 rows) — many small queries still hit the session cap. Batch instead of looping.edp-token lives ~10 minutes. A script that reads it once and runs for an hour dies mid-batch with a 401. workspace_cdp.token() re-reads within 60s of expiry — use it per request rather than caching the string.403 insufficient_scope and CUSIP/ISIN symbology is unentitled. Probe the endpoint and read the error; never assume a dataset is available because it exists in the docs.errors array when unentitled, and datagrid returns null data with messages.codes of -2 ("empty") for fields that do not apply. Read errors and messages.codes, not just the status line.fetch() from the Workspace tab is CORS-blocked and fails as a bare "Failed to fetch" that looks like a network outage. Workspace-internal endpoints must be called from a tab on their own origin — that is what in_page_fetch() does.SCREEN(U(IN(DEALS)) ...) universes are rejected by the public datagrid (error 218) and only work through the internal datacloud endpoint via path 3.references/codebook.md..head() or .sample() inspection → STOP. Handing over uninspected data gives the user undetected quality problems — unhelpful on its own terms.session.desktop.workspace config, or reference /Applications/Refinitiv Workspace.app, on Linux → STOP. There is no desktop session on this platform; the config will fail at connect time.Before EVERY data retrieval claim, verify the following:
For ld.get_data() (fundamentals/ESG):
.head() or .sample() executedFor ld.get_history() (time series):
For symbol_conversion.Definition() (mapping):
For ALL queries:
open_session() at start, close_session() at endTo get started with LSEG Data Library, initialize a session and execute queries:
import lseg.data as ld
# Initialize session
ld.open_session()
# Get fundamentals
df = ld.get_data(
universe=[‘AAPL.O’, ‘MSFT.O’],
fields=[‘TR.CompanyName’, ‘TR.Revenue’, ‘TR.EPS’]
)
print(df.head()) # Inspect sample data
# Get historical prices
prices = ld.get_history(
universe=’AAPL.O’,
fields=[‘OPEN’, ‘HIGH’, ‘LOW’, ‘CLOSE’, ‘VOLUME’],
start=‘2023-01-01’,
end=‘2023-12-31’
)
print(prices.head()) # Inspect sample data
# Close session
ld.close_session()
Four options. The first two are for the lseg.data library; the last two need no credentials of your own.
Platform session (works on every OS) — config file or environment variables, below. This is the only lseg.data session type available on Linux.
Desktop session — requires the Refinitiv Workspace desktop app running locally. Windows/macOS only; not an option on Linux.
Borrowed browser session — no credentials at all: scripts/workspace_cdp.py lifts the access token out of a signed-in Workspace Web tab. Use this when machine credentials are unavailable or expired. See references/workspace-web-cdp.md.
Codebook (hosted) — LSEG's own JupyterHub inside Workspace Web. Its kernels
open a pre-authenticated DesktopSession named codebook on LSEG's servers, so
it needs no local credentials and no local Workspace app, and it does not consume
the one-session platform quota below. Same entitlements as the platform session,
not broader. See references/codebook.md.
The rest of this section is about getting a platform session working, which is where all the sharp edges are.
Credentials live in agenix as lseg-credentials, decrypted to
$LSEG_CREDENTIALS_FILE (mode 400). It is a shell-sourceable file, so source
it, do not cat it into a variable:
set -a; . "$LSEG_CREDENTIALS_FILE"; set +a # exports LSEG_APP_KEY / LSEG_USERNAME / LSEG_PASSWORD
THE VARIABLE NAMES DO NOT MATCH THE LIBRARY'S. The secret exports LSEG_*;
everything below documents RDP_*. You must map them at the call site. Reading
this section and exporting RDP_APP_KEY from a file that defines LSEG_APP_KEY
gets you an empty environment and a session that fails on first query.
Before 2026-07-27 these existed only as plaintext in mbp:~/projects/svb/.envrc,
so anything running on another machine had no credentials at all. If a lookup
comes back empty, check that host's rebuild is current before concluding the
account is unentitled.
platform.Password DOES NOT EXISTThe config-file example below hides the programmatic form, and the obvious guess
is wrong. In lseg-data 2.1.1 the class is GrantPassword:
import lseg.data as ld
from lseg.data.session import platform
s = platform.Definition(
app_key=os.environ["LSEG_APP_KEY"],
grant=platform.GrantPassword(username=os.environ["LSEG_USERNAME"],
password=os.environ["LSEG_PASSWORD"]),
).get_session()
s.open()
ld.session.set_default(s)
platform exports exactly three names — ClientCredentials, Definition,
GrantPassword. Check dir() before trusting a class name from the docs.
signon_control=TrueThe machine ID allows a single concurrent platform session, and the library
default is signon_control=False, which does not queue — it fails:
LDError: You authorised with signon_control=False. Session quota is reached.
If you want to open session close the previous opened.
Any earlier session that was not closed cleanly (a crashed script, another shell,
a background job) holds the quota until it times out. Pass signon_control=True
to take the signon over instead:
s = platform.Definition(
app_key=os.environ["LSEG_APP_KEY"],
grant=platform.GrantPassword(username=os.environ["LSEG_USERNAME"],
password=os.environ["LSEG_PASSWORD"]),
signon_control=True, # <- take over rather than fail
).get_session()
s.open()
if str(s.open_state) != "OpenState.Opened": # open() does not raise; see above
raise RuntimeError(f"session failed: {s.open_state}")
ld.session.set_default(s)
Corollary: two local scripts cannot query at once. If you need a second
concurrent path — an interactive query while a long batch runs — use Codebook,
which authenticates as a separate DesktopSession and does not draw on this
quota (see Refinitiv Codebook below).
open_session() DOES NOT RAISE ON FAILUREWith no config and no credentials it falls back to a desktop session, tries
http://localhost:9000/api/handshake (LSEG Workspace running locally), logs the
connection failure, and returns normally. The error only surfaces on the
first query as ValueError: Session is not opened.
So open_session() returning is NOT evidence of a session. This is the same
silent-failure shape as the Iron Law above, one layer earlier: verify by issuing
a cheap query (TR.PriceClose on a liquid RIC) and inspecting the value.
Configure LSEG authentication using either a config file or environment variables.
Create lseg-data.config.json:
{
“sessions”: {
“default”: “platform.ldp”,
“platform”: {
“ldp”: {
“app-key”: “YOUR_APP_KEY”,
“username”: “YOUR_MACHINE_ID”,
“password”: “YOUR_PASSWORD”
}
}
}
}
Set the following environment variables for LSEG authentication:
# Configure LSEG credentials via environment variables
export RDP_USERNAME=”YOUR_MACHINE_ID”
export RDP_PASSWORD=”YOUR_PASSWORD”
export RDP_APP_KEY=”YOUR_APP_KEY”
| API | Use Case | Example |
|-----|----------|---------|
| ld.get_data() | Point-in-time data | Fundamentals, ESG scores |
| ld.get_history() | Time series | Historical prices, OHLCV |
| ld.news.get_headlines() | News headlines | Company news, topic filtering |
| symbol_conversion.Definition() | ID mapping | RIC ↔ ISIN ↔ CUSIP |
| Prefix | Type | Example |
|--------|------|---------|
| TR. | Refinitiv fields | TR.Revenue, TR.EPS |
| TR.MnA | Mergers & Acquisitions | TR.MnAAcquiror, TR.MnADealValue |
| TR.NI | Equity/New Issues (IPOs) | TR.NIIssuer, TR.NIOfferPrice |
| TR.JV | Joint Ventures/Alliances | TR.JVDealName, TR.JVStatus |
| TR.SACT | Shareholder Activism | TR.SACTLeadDissident |
| TR.PP | Poison Pills | TR.PPPillAdoptionDate |
| TR.LN | Syndicated Loans | TR.LNTotalFacilityAmount |
| TR.PJF | Infrastructure/Project Finance | TR.PJFProjectName |
| TR.PEInvest | Private Equity/Venture Capital | TR.PEInvestRoundDate |
| TR.Muni | Municipal Bonds | TR.MuniIssuerName |
| CF_ | Composite (real-time) | CF_LAST, CF_BID |
| Suffix | Exchange | Example |
|--------|----------|---------|
| .O | NASDAQ | AAPL.O |
| .N | NYSE | IBM.N |
| .L | London | VOD.L |
| .T | Tokyo | 7203.T |
| Endpoint | Limit |
|----------|-------|
| get_data() | 10,000 data points/request |
| get_history() | 3,000 rows/request |
| Session | 500 requests/minute |
references/fundamentals.md - Financial statement fields, ratios, estimatesreferences/esg.md - ESG scores, pillars, controversiesreferences/symbology.md - RIC/ISIN/CUSIP conversionreferences/short-interest.md - TR.ShortInterest: the only working field, the delisted-instrument coverage cliff, and the gap vs Compustatreferences/pricing.md - Historical prices, real-time datareferences/screening.md - Stock screening with Screener objectreferences/fscreen.md - Fund screening (ETFs, mutual funds) with FSCREEN appreferences/fund-details.md - Fund details and characteristicsreferences/news.md - News headlines, pagination, query syntaxreferences/mna.md - Mergers & acquisitions deals (SDC Platinum, 2,683 fields)references/equity-new-issues.md - IPOs, follow-ons, equity offerings (SDC Platinum, 1,708 fields)references/joint-ventures.md - Joint ventures, strategic alliances (SDC Platinum, 301 fields)references/corporate-governance.md - Shareholder activism, poison pills (SDC Platinum)references/syndicated-loans.md - Syndicated loan deals (SDC Platinum)references/infrastructure.md - Infrastructure/project finance deals (SDC Platinum)references/private-equity.md - Private equity/venture capital investments (SDC Platinum)references/municipal-bonds.md - Municipal bond issuances (SDC Platinum)references/workspace-web-cdp.md - Driving the Workspace web client over CDP: session setup, token lifting, endpoint matrix, entitlement gotchasreferences/codebook.md - Codebook (hosted JupyterHub): REST surface, and the kernel-execution blockerreferences/api-discovery.md - Reverse-engineering APIs via CDP network monitoringreferences/troubleshooting.md - Common issues and solutionsreferences/wrds-comparison.md - LSEG vs WRDS data mappingexamples/historical_pricing.ipynb - Historical price retrievalexamples/fundamentals_query.py - Fundamental data patternsexamples/stock_screener.ipynb - Dynamic stock screeningscripts/test_connection.py - Validate connectivity. No args tests the lseg.data platform session; --browser tests the CDP/Workspace-Web path.scripts/workspace_cdp.py - Drive Workspace Web over CDP: token, datagrid, history, symbology, search, sdc-screen, deal-data, fetch. Importable as a module or usable as a CLI.Deal-level SDC work is two steps — sdc_deal_ids() resolves a SCREEN(U(IN(DEALS)) ...) universe to deal IDs, then deal_data() fetches field values for them as <id>@DEALID. See references/workspace-web-cdp.md.
LSEG API samples at ~/resources/lseg-samples/:
Example.RDPLibrary.Python/ - Core API examplesExamples.DataLibrary.Python.AdvancedUsecases/ - Advanced patternsArticle.DataLibrary.Python.Screener/ - Stock screeningHosted JupyterLab with a pre-authenticated, fully entitled refinitiv.data session:
https://workspace.refinitiv.com/codebook/python3 and python3_legacy{name='codebook'})# Inside a Codebook notebook, the session opens with Workspace auth
import refinitiv.data as rd
rd.open_session() # name='codebook'
df = rd.news.get_headlines('R:AAPL.O AND SUGGAC', count=10)
Codebook cannot be driven for computation. Its contents/kernels/sessions REST API works with the browser's cookies, but the kernel WebSocket is refused server-side (close 1006) — including through Codebook's own JupyterLab UI, where a submitted cell sits at [*] forever. Use it as a file exchange (push a notebook, the user runs it, pull back the outputs) and read the user's existing notebooks as worked examples. Full detail and the re-test diagnostic: references/codebook.md.
Note: Codebook uses refinitiv.data (older name) rather than lseg.data. Both APIs are equivalent.
Confirmed working 2026-07-27. rd.open_session() there returns a
DesktopSession named codebook — a different session class from the
PlatformSession a local lseg-data script opens against the RDP machine ID.
Two practical consequences:
references/short-interest.md.Codebook is a JupyterLab in the browser, so it can be driven over CDP without clicking, via the Jupyter REST + WebSocket API. One trap makes this fail silently on the first try:
The kernel WebSocket host is NOT the page host. Read wsUrl from the
jupyter-config-data element rather than assuming location.host — it points at
wss://amers1-streaming-io.platform.refinitiv.com/..., and connecting to
workspace.refinitiv.com just errors with no message. The same element carries
the token the socket needs as a ?token= query param.
const cfg = JSON.parse(document.getElementById('jupyter-config-data').textContent);
// POST {name:'python3'} to cfg.baseUrl + 'api/kernels' with X-XSRFToken from the _xsrf cookie,
// then open: `${cfg.wsUrl}api/kernels/${kernelId}/channels?token=${cfg.token}`
// send an execute_request on channel 'shell'; collect 'stream' msgs until status.execution_state === 'idle'
First load spawns the server ("Preparing your CodeBook environment", a minute or
two) and the URL sits at /hub/spawn-pending/<user> until ready. Shut the kernel
down (DELETE api/kernels/<id>) when finished; the server itself idle-culls.
amers1 is the Americas region — read it from cfg.wsUrl, never hardcode it.
The full copy-pasteable recipe lives in references/codebook.md (added by
PR #95), together with the spawn/XSRF gotchas and why this failure mode is so
easy to misread: the wrong host returns no handshake response at all, which is
indistinguishable from a proxy blocking the upgrade.
When querying market data, account for current date context and market data lag.
Market data typically has T-1 availability, meaning today’s data becomes available tomorrow. Adjust date ranges accordingly.
Use current date context when querying historical prices:
from datetime import datetime, timedelta
# Get recent market data
end_date = datetime.now()
start_date = end_date - timedelta(days=365)
# Adjust to exclude recent data (T-1 for market data availability)
end_date = end_date - timedelta(days=1)
df = ld.get_history(
universe=”AAPL.O”,
fields=[‘CLOSE’],
start=start_date.strftime(‘%Y-%m-%d’),
end=end_date.strftime(‘%Y-%m-%d’)
)
Remember: Always account for the T-1 lag in market data availability.
development
Build the meeting-level proxy-voting × ownership panel on the WRDS SGE grid — ISS N-PX fund votes reduced to (item × block) direction cells, joined to institutional and mutual-fund ownership. Use when working with risk.voteanalysis_npx, N-PX fund-level votes, ISS→CRSP fund linking, index/passive/active voting blocks, or a proxy-voting panel that needs ownership attached.
development
Use when "CRSP CIZ", "CRSP v2", "CRSP flat file format 2.0", "crsp.dsf_v2 / msf_v2", "StkDlySecurityData", "StkMthSecurityData", "StkSecurityInfoHist", "stocknames_v2", "DlyRet / MthRet / DlyPrc / MthPrc", "SHRCD or EXCHCD equivalent in new CRSP", "SIZ to CIZ migration", "CRSP data after 2024", "CRSP delisting returns", "CRSP cumulative adjustment factors", "CRSP index INDNO / INDFAM", or any CRSP stock/index query where the legacy SIZ column names no longer exist.
development
Use when linking or deduping datasets by entity name rather than a shared key — 'fuzzy match', 'fuzzy name matching', 'entity resolution', 'record linkage', 'match company/person names', 'dedupe entity names', 'name-based join', 'bridge identifiers' (CIK ↔ permno ↔ gvkey ↔ wficn ↔ EIN ↔ personid), or any use of char n-gram TF-IDF, cosine similarity on names, `sparse_dot_topn`, or RapidFuzz at scale.
development
Use when building a publication-quality table in Python — 'regression table', 'results table', 'summary statistics table', 'etable', 'coefplot', 'great_tables', 'GT', 'gt table', 'format a table for the paper', 'export table to LaTeX/HTML', significance stars, spanners, or column formatting for a table headed into a paper, slide deck, or notebook.