Data Sources Overview#
Background#
The paper Can ChatGPT Forecast Stock Price Movements? (Lopez-Lira and Tang, 2023) investigates whether a large language model can interpret financial news headlines and translate those interpretations into profitable trading signals. Each headline is presented to ChatGPT, which classifies it as implying a stock price increase (YES), decrease (NO), or no clear direction (UNKNOWN). These classifications are then used to construct daily long/short trading portfolios.
This case study replicates two of the paper’s core results:
Table 1 – predictive performance of the headline-based strategy
Figure 5 – cumulative returns under several sample restrictions
This first notebook gives a tour of the two underlying datasets used in the replication: daily stock data from CRSP and financial news headlines from independently scraped news sources (PR Newswire and GDELT), enriched with entity metadata from RavenPack via WRDS. RavenPack metadata provides the ticker mapping and relevance scores used for filtering, but the headline text itself comes from free sources – a distinction that matters because RavenPack’s licensing prohibits sending its data to third-party servers like OpenAI.
Setup#
from pathlib import Path
import pandas as pd
from IPython.display import Markdown, display
from settings import config
DATA_DIR = Path(config("DATA_DIR"))
OUTPUT_DIR = Path(config("OUTPUT_DIR"))
MANUAL_DATA_DIR = Path(config("MANUAL_DATA_DIR"))
paths = {
"CRSP Stock Data": DATA_DIR / "CRSP_stock_daily.parquet",
"RavenPack Metadata": DATA_DIR / "RAVENPACK.parquet",
"Scraped Headlines": DATA_DIR / "RAVENPACK_scraped.parquet",
"Cleaned Headlines": DATA_DIR / "RAVENPACK_cleaned.parquet",
"Daily Headline Scores": DATA_DIR / "daily_headline_polarity.parquet",
"Portfolio Returns": DATA_DIR / "portfolio_daily_returns.parquet",
"Table1 (Paper Sample)": OUTPUT_DIR / "table1_overnight_paper_sample.csv",
"Table1 (Full Sample)": OUTPUT_DIR / "table1_overnight_full_sample.csv",
"Paper Table Data": MANUAL_DATA_DIR / "paper_table1.csv",
}
status = pd.DataFrame(
{"file": list(paths.keys()), "exists": [p.exists() for p in paths.values()]}
)
display(status)
| file | exists | |
|---|---|---|
| 0 | CRSP Stock Data | True |
| 1 | RavenPack Metadata | True |
| 2 | Scraped Headlines | True |
| 3 | Cleaned Headlines | True |
| 4 | Daily Headline Scores | True |
| 5 | Portfolio Returns | True |
| 6 | Table1 (Paper Sample) | True |
| 7 | Table1 (Full Sample) | True |
| 8 | Paper Table Data | True |
1. CRSP Stock Data#
CRSP (Center for Research in Security Prices) is the standard academic database for U.S. equity data, maintained by the University of Chicago. The pull fetches daily stock-level data – returns, open/close prices, and shares outstanding – needed to compute portfolio returns from the ChatGPT trading signals.
The SQL WHERE clause restricts the pull to match the paper’s
universe (Section 4, common stocks with share codes 10 and 11):
WHERE
primaryexch IN ('N', 'A', 'Q')
AND conditionaltype = 'RW'
AND tradingstatusflg = 'A'
AND sharetype = 'NS'
AND securitytype = 'EQTY'
AND securitysubtype = 'COM'
AND usincflg = 'Y'
AND issuertype IN ('ACOR', 'CORP')
AND dlycaldt >= '{start_date}'
AND dlycaldt <= '{end_date}'
primaryexch IN ('N', 'A', 'Q')– only stocks listed on the three major U.S. exchanges: NYSE (N), AMEX (A), and NASDAQ (Q).conditionaltype = 'RW'– keeps only “Regular Way” traded securities, i.e. standard exchange-traded stocks. In the legacy CRSP format this is equivalent to restricting to main exchange codes (exchcdin 1, 2, 3).tradingstatusflg = 'A'– restricts to actively trading securities, excluding halted or delisted stocks.sharetype = 'NS',securitytype = 'EQTY',securitysubtype = 'COM',usincflg = 'Y',issuertype IN ('ACOR', 'CORP')– restricts to domestic common stocks, the CRSP V2 equivalent of share codes 10 and 11. This excludes ETFs, REITs, ADRs, closed-end funds, and other non-common-equity securities.dlycaldt >= ... AND dlycaldt <= ...– bounds the date range. The paper’s sample runs from October 2021 to May 2024, chosen so that all headlines post-date GPT-4’s training data cutoff (September 2021), avoiding look-ahead bias.
In short: the CRSP pull provides the daily price and return data for the universe of ordinary, actively-traded U.S. common stocks on the three major exchanges, over the paper’s sample period. This data is then joined with the headline-level ChatGPT scores to compute the long/short portfolio returns analyzed in Table 1 and Figure 5.
crsp = (
pd.read_parquet(paths["CRSP Stock Data"])
if paths["CRSP Stock Data"].exists()
else pd.DataFrame()
)
print("CRSP shape:", crsp.shape)
display(crsp.head())
CRSP shape: (4479130, 13)
| permno | permco | ticker | primaryexch | sharetype | securitytype | securitysubtype | usincflg | issuertype | date | dlycap | dlyopen | dlyclose | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10026 | 7976 | JJSF | Q | NS | EQTY | COM | Y | CORP | 2021-10-01 | 2932065.76 | 153.85 | 153.64 |
| 1 | 10028 | 7978 | ELA | A | NS | EQTY | COM | Y | CORP | 2021-10-01 | 109584.75 | 4.11 | 4.07 |
| 2 | 10032 | 7980 | PLXS | Q | NS | EQTY | COM | Y | CORP | 2021-10-01 | 2554520.76 | 89.59 | 91.08 |
| 3 | 10044 | 7992 | RMCF | Q | NS | EQTY | COM | Y | CORP | 2021-10-01 | 43174.2 | 7.36 | 7.05 |
| 4 | 10051 | 7999 | HNGR | N | NS | EQTY | COM | Y | CORP | 2021-10-01 | 864334.6 | 22.19 | 22.34 |
import create_summary_statistics as css
crsp_summary = css.create_crsp_summary()
display(crsp_summary)
del crsp
| Metric | Mean | SD | P25 | Median | P75 | |
|---|---|---|---|---|---|---|
| 0 | Market Cap ($M) | 12.481358 | 94.483500 | 0.115490 | 0.652421 | 3.780629 |
| 1 | Daily Return (%) | -0.091481 | 4.670486 | -1.622361 | 0.000000 | 1.345291 |
2. RavenPack Metadata (Entity Mapping and Filters)#
RavenPack provides real-time news analytics covering over 40,000
sources. In this pipeline, the RavenPack pull from WRDS
(RAVENPACK.parquet) is used only as a metadata lookup – it
supplies entity identifiers (rp_entity_id), ticker mappings
(map_ticker), relevance scores, and deduplication keys. The
headline text actually sent to OpenAI comes from independently
scraped sources (see Section 3 below).
The RavenPack filters applied match the paper’s specifications:
Relevance score of 100
News reports and press releases only
Exclude headlines categorized as “stock gain” or “stock loss”
rp_full = (
pd.read_parquet(paths["RavenPack Metadata"])
if paths["RavenPack Metadata"].exists()
else pd.DataFrame()
)
print("RavenPack metadata shape:", rp_full.shape)
display(rp_full.head())
RavenPack metadata shape: (1143333, 7)
| rp_entity_id | rpa_date_utc | timestamp_utc | map_ticker | entity_name | headline | css | |
|---|---|---|---|---|---|---|---|
| 0 | 000127 | 2023-02-01 | 2023-02-01 11:15:37.880 | BRIS | PT Bank Syariah Indonesia Tbk | Press Release: Fitch Affirms Bank Syariah Indo... | 0.04 |
| 1 | 000127 | 2023-11-03 | 2023-11-03 12:16:16.366 | BRIS | PT Bank Syariah Indonesia Tbk | Press Release: Fitch Upgrades Bank Syariah Ind... | 0.02 |
| 2 | 000127 | 2024-10-23 | 2024-10-23 10:50:44.526 | BRIS | PT Bank Syariah Indonesia Tbk | Press Release: Fitch Upgrades Bank Syariah Ind... | 0.02 |
| 3 | 000127 | 2024-05-16 | 2024-05-16 02:15:10.040 | BRIS | PT Bank Syariah Indonesia Tbk | Press Release: Fitch Affirms Bank Syariah Indo... | 0.04 |
| 4 | 0005DF | 2025-09-01 | 2025-09-01 06:00:15.367 | PRD | Predator Oil & Gas Holding PLC | Predator Oil & Gas Holdings PLC Completion of ... | 0.06 |
news_by_year = css.create_news_by_year()
display(Markdown("### Headlines by Year"))
display(news_by_year)
Headlines by Year
| Year | Headlines | Firms | Days | Per Day | Per Firm | |
|---|---|---|---|---|---|---|
| 0 | 2021 | 83912 | 12228 | 92 | 912.1 | 6.9 |
| 1 | 2022 | 279018 | 19714 | 363 | 768.6 | 14.2 |
| 2 | 2023 | 263040 | 20321 | 364 | 722.6 | 12.9 |
| 3 | 2024 | 238383 | 19526 | 366 | 651.3 | 12.2 |
| 4 | 2025 | 233191 | 20376 | 365 | 638.9 | 11.4 |
| 5 | 2026 | 45789 | 10422 | 59 | 776.1 | 4.4 |
news_by_timing = css.create_news_by_timing()
display(Markdown("### Headlines by Release Timing"))
display(news_by_timing)
del rp_full
Headlines by Release Timing
| Timing | Headlines | % Total | Firms | Per Day | |
|---|---|---|---|---|---|
| 1 | Overnight | 1020420 | 89.2 | 30164 | 636.2 |
| 0 | Intraday | 122913 | 10.8 | 17841 | 81.2 |
The majority of news headlines are released outside regular trading hours, which motivates the focus on overnight signals in the trading strategy.
3. Scraped Headlines (Sent to OpenAI)#
The headline text used for LLM classification comes from the upstream
news_headlines chartbook pipeline, which produces the
scraped_headlines_with_rp_metadata dataset. This dataset joins
independently scraped headlines from two free sources:
PR Newswire – press releases scraped via sitemap crawling (coverage from January 2010)
GDELT – Global Knowledge Graph headlines filtered to S&P 500 companies via BigQuery (coverage from February 2015)
These scraped headlines are fuzzy-matched against the RavenPack DJ
Press Release archive to inherit entity metadata (tickers, relevance
scores, event similarity keys). Crucially, the headline column
contains text from the free scraped sources and is safe to send to
external APIs like OpenAI – unlike RavenPack’s own headline text,
which is subject to licensing restrictions.
The merge pipeline (merge_scraped_headlines.py) applies the same
RavenPack filters listed above and outputs RAVENPACK_scraped.parquet.
rp_scraped = (
pd.read_parquet(paths["Scraped Headlines"])
if paths["Scraped Headlines"].exists()
else pd.DataFrame()
)
print("Scraped headlines shape:", rp_scraped.shape)
display(rp_scraped.head())
del rp_scraped
Scraped headlines shape: (222036, 7)
| rp_entity_id | rpa_date_utc | timestamp_utc | map_ticker | entity_name | headline | css | |
|---|---|---|---|---|---|---|---|
| 0 | 9CA8F4 | 2021-10-01 | 2021-10-01 10:00:00.562 | KW | Kennedy-Wilson Holdings Inc. | Kennedy Wilson Acquires Two Multifamily Commun... | 0.10 |
| 1 | 8665BA | 2021-10-01 | 2021-10-01 10:00:02.797 | TMO | Thermo Fisher Scientific Inc. | Thermo Fisher Scientific Opens Biologics Manuf... | 0.00 |
| 2 | 723548 | 2021-10-01 | 2021-10-01 10:00:04.814 | GNL | Global Net Lease Inc. | Global Net Lease, Inc. Announces Common Stock ... | 0.04 |
| 3 | D15833 | 2021-10-01 | 2021-10-01 10:32:58.298 | GIH | Gold Resource Corp. | Gold Resource Corporation to Hold Q3 2021 Conf... | 0.06 |
| 4 | 7D0853 | 2021-10-01 | 2021-10-01 10:54:13.413 | PLNH | Planet 13 Holdings Inc. | Planet 13 Completes Acquisition of Florida Can... | 0.00 |
4. Cleaned Headlines#
The cleaning pipeline (clean_ravenpack.py) reads the scraped
headlines (RAVENPACK_scraped.parquet) and applies several filters
to match the paper’s methodology:
Ticker mapping – filter to tickers present in the CRSP universe
Deduplication – remove near-duplicate firm-day headlines using Optimal String Alignment (OSA) similarity (threshold > 0.60)
Timing alignment – convert timestamps to Eastern Time; exclude intraday headlines (9 AM – 4 PM ET); roll overnight headlines (after 4 PM) to the next trading day
This replication focuses exclusively on overnight news (the paper’s primary analysis in Table 1 Panel A and Figure 5). Intraday headlines are dropped during cleaning, and no TAQ-based intraday return windows are computed.
The output (RAVENPACK_cleaned.parquet) contains overnight-only
scraped headline text ready for submission to OpenAI.
from notebook_helper import get_rp_timing_stats
rp_clean = (
pd.read_parquet(paths["Cleaned Headlines"])
if paths["Cleaned Headlines"].exists()
else pd.DataFrame()
)
print("Cleaned headlines shape:", rp_clean.shape)
display(rp_clean.head())
Cleaned headlines shape: (50995, 9)
| rp_entity_id | map_ticker | entity_name | timestamp_utc | rpa_date_utc | timestamp_et | headline_date | headline | css | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 9CA8F4 | KW | Kennedy-Wilson Holdings Inc. | 2021-10-01 10:00:00.562 | 2021-10-01 | 2021-10-01 06:00:00.562000-04:00 | 2021-10-01 | Kennedy Wilson Acquires Two Multifamily Commun... | 0.1 |
| 1 | 8665BA | TMO | Thermo Fisher Scientific Inc. | 2021-10-01 10:00:02.797 | 2021-10-01 | 2021-10-01 06:00:02.797000-04:00 | 2021-10-01 | Thermo Fisher Scientific Opens Biologics Manuf... | 0.0 |
| 2 | 9465C9 | COGT | Cogent Biosciences Inc. | 2021-10-01 11:00:05.031 | 2021-10-01 | 2021-10-01 07:00:05.031000-04:00 | 2021-10-01 | Cogent Biosciences to Present Preclinical Data... | 0.0 |
| 3 | 14C7B2 | KDP | Keurig Dr Pepper Inc. | 2021-10-01 11:00:06.079 | 2021-10-01 | 2021-10-01 07:00:06.079000-04:00 | 2021-10-01 | Keurig Dr Pepper Announces New Share Repurchas... | 0.1 |
| 4 | 1272B3 | AMRC | Ameresco Inc. | 2021-10-01 11:15:00.310 | 2021-10-01 | 2021-10-01 07:15:00.310000-04:00 | 2021-10-01 | Ameresco to Participate at Upcoming October Co... | 0.0 |
if not rp_clean.empty:
stats, timing_table = get_rp_timing_stats(rp_clean)
display(Markdown("### Timing Diagnostics"))
display(stats)
display(timing_table)
del rp_clean
Intraday rows remaining (expected ~0): 0
Timing Diagnostics
| n_rows | n_tickers | min_ts | max_ts | min_headline_date | max_headline_date | |
|---|---|---|---|---|---|---|
| 0 | 50995 | 3085 | 2021-10-01 06:00:00.562000-04:00 | 2026-01-30 17:00:00.348000-05:00 | 2021-10-01 | 2026-01-31 |
| sample | rows total | headline dates rolled over | |
|---|---|---|---|
| 0 | Oct 2021 - May 2024 | 33509 | 7156 |
| 1 | Oct 2021 - Feb 2026 | 50995 | 11045 |
Summary#
This notebook introduced the two primary data sources used in the replication:
CRSP provides daily stock prices and market capitalization for firms traded on NYSE, AMEX, and NASDAQ.
Scraped headlines (from PR Newswire and GDELT) provide the news text sent to OpenAI for classification. These are free-source headlines matched against RavenPack metadata for entity identification, ticker mapping, and deduplication.
The next notebook walks through how these headlines are submitted to OpenAI for classification and how the resulting signals are translated into trading portfolios.