Replicate ChatGPT Price Forecasting#
This notebook gives a concise, reader-friendly tour of the project workflow and the core outputs used in the replication.
Summary#
The Lopez-Lira and Tang (2023) paper āCan ChatGPT Forecast Stock Price Movements? Return Predictability and Large Language Modelsā documents the capability of LLMs, such as ChatGPT, to predict stock market reactions from news headlines without direct financial training. The papers results suggest forecasting ability generally increases with model size and strategy returns decline as LLM usage within the finanical domain increases. We aim to replicate their results, specifically looking at strategy hit rates, and portfolio returns results.
Methodology at a glance#
Data foundation: pull CRSP prices and RavenPack headlines from WRDS.
Data cleaning: map entities to tickers, remove low-quality/duplicate signals, and align headline timing to trading logic.
NLP labeling: submit batched headlines to OpenAI and parse labels into structured sentiment scores.
Portfolio construction: convert firm-day sentiment into long/short return series under multiple sample restrictions.
Reporting: generate summary tables and figures used in the write-up.
Pipeline context for this notebook#
The data shown below comes from the doit pipeline in dodo.py.
pull:crsp_stockrunspull_CRSP_stock.pyand writesCRSP_stock_daily.parquetandCRSP_unique_tickers.parquet.pull:ravenpackrunspull_ravenpack.pyand writesRAVENPACK.parquet.clean_data:clean_ravenpackrunsclean_ravenpack.pyand writesRAVENPACK_cleaned.parquet.process:generate_batched_requests,process:submit_headlines_to_openai, andprocess:create_firm_day_scoreproducedaily_headline_polarity.parquet.Downstream scripts
create_portfolios.pyandcreate_table1.pygenerate return panels and final table CSVs used in this guide.
Step 0. Setup and Key Paths#
Start by confirming which pipeline artifacts exist before loading data. This prevents downstream errors and makes the notebook reproducible across machines and collaborators.
The code imports project settings from settings.py, builds the canonical file map, and prints an existence table so we can verify which stages of the pipeline have already been run.
import json
from pathlib import Path
import pandas as pd
from notebook_helper import (
generate_single_request_jsonl,
get_rp_timing_stats,
printable_table,
)
from settings import config
MANUAL_DATA_DIR = Path(config("MANUAL_DATA_DIR"))
DATA_DIR = Path(config("DATA_DIR"))
OUTPUT_DIR = Path(config("OUTPUT_DIR"))
paths = {
"CRSP Stock Data": DATA_DIR / "CRSP_stock_daily.parquet",
"RavenPack Full": DATA_DIR / "RAVENPACK.parquet",
"RavenPack Clean": DATA_DIR / "RAVENPACK_cleaned.parquet",
"Daily Headline Scores": DATA_DIR / "daily_headline_polarity.parquet",
"Portfolio Returns": DATA_DIR / "portfolio_daily_returns.parquet",
"Table1 (Oct 2021 - May 2024)": OUTPUT_DIR / "table1_overnight_paper_sample.csv",
"Table1 (Oct 2021 - March 2026)": OUTPUT_DIR / "table1_overnight_full_sample.csv",
"Paper Table Data": MANUAL_DATA_DIR / "paper_table1.csv",
}
pd.DataFrame(
{
"file": list(paths.keys()),
"exists": [p.exists() for p in paths.values()],
}
)
| file | exists | |
|---|---|---|
| 0 | CRSP Stock Data | True |
| 1 | RavenPack Full | True |
| 2 | RavenPack Clean | True |
| 3 | Daily Headline Scores | True |
| 4 | Portfolio Returns | True |
| 5 | Table1 (Oct 2021 - May 2024) | True |
| 6 | Table1 (Oct 2021 - March 2026) | True |
| 7 | Paper Table Data | True |
Step 1. Load Cleaned RavenPack and CRSP#
In order to replicate the results in this paper, we need market data from CRSP and news data from RavenPack. We are ignoring intraday data. Additionally, we use the RPA Entity Mapping File (wrds_rpa_company_mappings) in order to facilitate the mapping of RavenPack entities to company tickers.
We pulled data between 2021-10-1 and 2026-03-01 for this project. Our replication is concerned with the paperās timebounds of 2021-10-01 to 2024-05-31, and additional analysis was completed on the full sample.
The CRSP dataset contains stock market data, including prices, returns, and shares outstanding, for securities traded on major U.S. exchanges. This dataset is essential for measuring market performance and calculating variables such as market equity.
The RavenPack dataset, using over 40,000 sources, provides real-time news analytics, including sentiment analysis and event data focused on business and financial applications. Data includes news and social media content, allowing for comprehensive analysis of financial markets.
The RPA Entity Mapping File provides a variety of security identifiers (ex. ISINs, CUSIPs, etc.) to allow you to identify the 90,000+ companies.
Pipeline step that generated this data
CRSP_stock_daily.parquetis generated bypull:crsp_stockviapull_CRSP_stock.py.RAVENPACK_cleaned.parquetis generated byclean_data:clean_ravenpackviaclean_ravenpack.py(afterpull:ravenpack).
Our pulled CRSP dataset is limited to the world of stocks specified in the paper:
WHERE
(
primaryexch IN ( 'N', 'A', 'Q' ) AND
conditionaltype = 'RW' AND
tradingstatusflg = 'A' AND
dlycaldt >= '{start_date}' AND
dlycaldt <= '{end_date}'
{permno_filter}
) ;
The clean_ravenpack process was designed to replicate the data cleaning procedure described in the paper. This includes filtering RavenPack data to match CRSP tickers, ensure a baseline relevancy rating of 0.60, deduplicating firm-day headlines using Optimal String Alignment (OSA) similarity, and aligning headline timestamps to Eastern Time. Intraday headlines are excluded, and overnight headlines are adjusted to ensure proper alignment with trading day per teh overnight cutoff at 4:00PM ET. This step ensures that the cleaned dataset is consistent with the methodology outlined in the paper.
rp = pd.read_parquet(paths["RavenPack Clean"]) if paths["RavenPack Clean"].exists() else pd.DataFrame()
crsp = pd.read_parquet(paths["CRSP Stock Data"]) if paths["CRSP Stock Data"].exists() else pd.DataFrame()
print("RavenPack cleaned shape:", rp.shape)
print("CRSP Stock Data shape:", crsp.shape)
display(rp.head(3))
display(crsp.head(3))
del crsp
RavenPack cleaned shape: (209714, 8)
CRSP Stock Data shape: (7467730, 8)
| rp_entity_id | map_ticker | entity_name | timestamp_utc | rpa_date_utc | timestamp_et | headline_date | headline | |
|---|---|---|---|---|---|---|---|---|
| 0 | 00067A | HUM | Humana Inc. | 2025-01-09 21:30:00.623 | 2025-01-09 | 2025-01-09 16:30:00.623000-05:00 | 2025-01-10 | Humana Inc. to Release Fourth Quarter 2024 Res... |
| 1 | 00067A | HUM | Humana Inc. | 2025-11-12 13:45:00.460 | 2025-11-12 | 2025-11-12 08:45:00.460000-05:00 | 2025-11-12 | ProgenyHealth Announces Collaboration with Hum... |
| 2 | 00067A | HUM | Humana Inc. | 2025-07-31 11:00:04.767 | 2025-07-31 | 2025-07-31 07:00:04.767000-04:00 | 2025-07-31 | Exact Sciences and Humana Expand Colorectal Ca... |
| permno | permco | ticker | primaryexch | date | dlycap | dlyopen | dlyclose | |
|---|---|---|---|---|---|---|---|---|
| 0 | 10026 | 7976 | JJSF | Q | 2021-10-01 | 2932065.76 | 153.85 | 153.64 |
| 1 | 10028 | 7978 | ELA | A | 2021-10-01 | 109584.75 | 4.11 | 4.07 |
| 2 | 10032 | 7980 | PLXS | Q | 2021-10-01 | 2554520.76 | 89.59 | 91.08 |
Headline Timing Diagnostics#
stats, table = get_rp_timing_stats(rp)
display(stats.style)
display(table.style)
Intraday rows remaining (expected ~0): 0
| n_rows | n_tickers | min_ts | max_ts | min_headline_date | max_headline_date | |
|---|---|---|---|---|---|---|
| 0 | 209714 | 5505 | 2021-09-30 20:21:21.593000-04:00 | 2026-02-28 18:00:03.460000-05:00 | 2021-10-01 | 2026-03-01 |
| sample | rows total | headline dates rolled over | |
|---|---|---|---|
| 0 | Oct 2021 - May 2024 | 130111 | 23798 |
| 1 | Oct 2021 - Feb 2026 | 209714 | 38707 |
Step 3. Generate Batched Requests (Single-Row Example)#
Pipeline step that generated this data
openai_headline_requests.*.jsonlfiles are generated byprocess:generate_batched_requestsviagenerate_batched_requests.py.id_to_row_mapping.*.jsonfiles are generated byprocess:generate_batched_requestsviagenerate_batched_requests.py.
This step demonstrates the generate_batched_requests.py logic on a single RavenPack row to replicate the pipeline behavior.
In order to retrieve OpenAI responses to our headline prompts, we must create batch files for asynchornous submission. The RavenPack data is broken up into chunks (40,000 being the default) and each request is a single line of json (referred to as JSONL). We also create a separate mapping file for ease of processing later on.
jsonl_content, mapping_content = generate_single_request_jsonl(rp.head(1).copy())
Wrote requests jsonl: /tmp/tmproknete3/openai_headline_requests.single.jsonl
Number of headlines batched: 1
Wrote id to row mapping json: /tmp/tmproknete3/id_to_row_mapping.single.json
print("JSONL content inline:")
display(jsonl_content)
JSONL content inline:
'{"custom_id": "rp-0", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-3.5-turbo", "temperature": 0, "messages": [{"role": "system", "content": "Forget all your previous instructions. Pretend you are a financial expert. You are a financial expert with stock recommendation experience. Answer \\"YES\\" if good news, \\"NO\\" if bad news, or \\"UNKNOWN\\" if uncertain in the first line. Then elaborate with one short and concise sentence on the next line."}, {"role": "user", "content": "Is this headline good or bad for the stock price of Humana Inc. in the short term?\\nHeadline: Humana Inc. to Release Fourth Quarter 2024 Results on February 11, 2025"}]}}\n'
print("JSONL content formatted:")
print(
json.dumps(
json.loads(jsonl_content), sort_keys=True, indent=2, separators=(",", ": ")
)
)
JSONL content formatted:
{
"body": {
"messages": [
{
"content": "Forget all your previous instructions. Pretend you are a financial expert. You are a financial expert with stock recommendation experience. Answer \"YES\" if good news, \"NO\" if bad news, or \"UNKNOWN\" if uncertain in the first line. Then elaborate with one short and concise sentence on the next line.",
"role": "system"
},
{
"content": "Is this headline good or bad for the stock price of Humana Inc. in the short term?\nHeadline: Humana Inc. to Release Fourth Quarter 2024 Results on February 11, 2025",
"role": "user"
}
],
"model": "gpt-3.5-turbo",
"temperature": 0
},
"custom_id": "rp-0",
"method": "POST",
"url": "/v1/chat/completions"
}
print("ID to Row Mapping:")
print(
json.dumps(
json.loads(mapping_content), sort_keys=True, indent=2, separators=(",", ": ")
)
)
ID to Row Mapping:
{
"rp-0": {
"date": "2025-01-10",
"entity_name": "Humana Inc.",
"ticker": "HUM"
}
}
Step 4. Submit Headlines to OpenAI Batch API#
Pipeline step that generated this data
openai_headline_batch_output.*.jsonlfiles are generated byprocess:submit_headlines_to_openaiviasubmit_headlines_to_openai.py.openai_headline_batch_metadata.*.jsonfiles are generated byprocess:submit_headlines_to_openaiviasubmit_headlines_to_openai.py.
All batch request files matching openai_headline_requests.*.jsonl are then submitted to OpenAI via the process:submit_headlines_to_openai step, with an SLA of 24 hours for a response. The pipeline blocks while waiting for a completion status for all submitted files. Results are written to the DATA_DIR as openai_headline_batch_output.*.jsonl with corresponding metadata files openai_headline_batch_metadata.*.json.
We prompt the LLM for one of three responses
YES: headline was deemed positive for stock price forecastNO: headline was deemed negative for stock price forecastUNKNOWN: headline was deemed neutral for stock price forecast
Below is a success response from OpenAI.
data = pd.read_json(DATA_DIR / "openai_headline_batch_output.1.jsonl", lines=True).head(1)
display(data.iloc[0]['response'])
{'status_code': 200,
'request_id': '20c04669-5262-44e6-8fca-cc48e5b5d47b',
'body': {'id': 'chatcmpl-DJUCVBfr6XgXN8m415PXlAhTb9NaR',
'object': 'chat.completion',
'created': 1773536695,
'model': 'gpt-3.5-turbo-0125',
'choices': [{'index': 0,
'message': {'role': 'assistant',
'content': 'UNKNOWN\nThe stock price may fluctuate depending on the content of the results and market expectations.',
'refusal': None,
'annotations': []},
'logprobs': None,
'finish_reason': 'stop'}],
'usage': {'prompt_tokens': 117,
'completion_tokens': 19,
'total_tokens': 136,
'prompt_tokens_details': {'cached_tokens': 0, 'audio_tokens': 0},
'completion_tokens_details': {'reasoning_tokens': 0,
'audio_tokens': 0,
'accepted_prediction_tokens': 0,
'rejected_prediction_tokens': 0}},
'service_tier': 'default',
'system_fingerprint': None}}
Step 5. Firm-Day Sentiment Output#
The next step in our pipeline processes the OpenAI responses, and aggregates the modelās rating of each headline.
Pipeline step that generated this data
daily_headline_polarity.parquetis generated byprocess:create_firm_day_scoreincreate_firmday_score.py
We parse each response, and if the answer for a headline was YES it gets a score of 1, NO gets a score of -1, and UNKNOWN gets a score of 0. We group the data set by date and ticker, and aggregate the total headlines, total score sum, and overall polarity (positive or negative). The results are written to DATA_DIR as daily_headline_polarity.parquet
import create_firmday_score as cfs
cfs.main()
[CHECK] outputs parsed unique custom_id: 209,710
[CHECK] outputs skipped: empty_choices=0, empty_content=0, unparseable=4
[CHECK] mapping rows unique custom_id: 209,714
[CHECK] after merge outputsāmapping:
_merge
both 209710
left_only 0
right_only 0
[CHECK] rows after dropping missing ticker/entity/date/headline: 209,710 (dropped 0)
[CHECK] trading days in CRSP calendar: 1,087 (min=2021-10-01, max=2026-01-30)
[CHECK] rows after dropping missing trade_date: 203,954 (dropped 5,756)
[CHECK] final firm-day rows: 176,968
[CHECK] firm-day date range: 2021-10-01 ā 2026-01-30
[CHECK] unique trading dates in firm-day: 1,087
Wrote: /home/tomhi/finmath/finm-32900/p05_lopez-lira_tang_2023/_data/daily_headline_polarity.parquet (rows=176,968)
scores = pd.read_parquet(paths["Daily Headline Scores"]) if paths["Daily Headline Scores"].exists() else pd.DataFrame()
print("Scores shape:", scores.shape)
display(scores.head(5).style)
if not scores.empty:
summary = (
scores["score"]
.value_counts(dropna=False)
.rename_axis("score")
.reset_index(name="count")
)
summary["share"] = (summary["count"] / summary["count"].sum()).round(4)
summary.sort_values("score", inplace=True, ignore_index=True)
summary = summary.rename(index={0: "NO", 1: "UNKNOWN", 2: "YES"})
display(summary.style)
Scores shape: (176968, 5)
| ticker | date | n_headlines | score_sum | score | |
|---|---|---|---|---|---|
| 0 | A | 2021-11-18 | 1 | 1 | 1 |
| 1 | A | 2021-11-23 | 2 | 1 | 1 |
| 2 | A | 2022-01-27 | 1 | 0 | 0 |
| 3 | A | 2022-02-16 | 1 | 1 | 1 |
| 4 | A | 2022-02-23 | 3 | 2 | 1 |
| score | count | share | |
|---|---|---|---|
| NO | -1 | 9296 | 0.052500 |
| UNKNOWN | 0 | 118923 | 0.672000 |
| YES | 1 | 48749 | 0.275500 |
Step 6. Portfolio Returns#
Pipeline step that generated this data
portfolio_daily_returns.parquetis generated byprocess:create_portfoliosviacreate_portfolios.py.
In order to actually replicate the paper results, we need to translate the headline scores into trading signals and build the following daily-rebalanced portfolios as described in the paper:
Long-Short Portfolio: buying positive predictions and selling negative predictions when both legs have at least two firms; otherwise entering just one leg.
Long-Only Portfolio: buying only positive predictions.
Short-Only Portfolio: selling only negative predictions.
Moreover, to replicate Figure 5 from the paper, which shows the cumulative return of the Long-Short portfolio with additional restrictions as described in the paper (we are excluding transaction costs):
Not Small: market capitalization above the 20th percentile
Price > 5: close price greater than $5 on previous day
port = pd.read_parquet(paths["Portfolio Returns"]) if paths["Portfolio Returns"].exists() else pd.DataFrame()
print("Portfolio returns:", port.shape)
display(port.head(5).style)
Portfolio returns: (1087, 20)
| date | n_neg | n_neu | n_pos | n_total | ret_long | n_long | n_short | ret_short | ret_ir_long | ret_ir_short | ret_ls_restricted | ret_ls_not_small | ret_ls_price_gt_5 | ret_mkt_vw | trade_long | trade_short | trade_ls | ret_ls | ret_ir_ls | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021-10-01 | 3 | 37 | 19 | 59 | 0.000000 | 0 | 0 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | True | True | True | 0.000000 | 0.000000 |
| 1 | 2021-10-04 | 3 | 83 | 37 | 123 | -0.001595 | 32 | 2 | -0.001542 | 0.003042 | 0.051936 | -0.004477 | -0.004432 | 0.000915 | -0.010693 | True | True | True | -0.003137 | 0.054978 |
| 2 | 2021-10-05 | 5 | 107 | 33 | 145 | 0.020661 | 29 | 5 | 0.026466 | 0.006686 | 0.001040 | 0.066331 | 0.066341 | 0.059321 | 0.006008 | True | True | True | 0.047127 | 0.007726 |
| 3 | 2021-10-06 | 3 | 113 | 45 | 161 | 0.009360 | 40 | 2 | 0.033815 | 0.008709 | 0.023499 | -0.013591 | -0.013591 | -0.015007 | 0.011786 | True | True | True | 0.043175 | 0.032208 |
| 4 | 2021-10-07 | 3 | 93 | 43 | 139 | 0.001959 | 41 | 1 | -0.054348 | 0.007832 | 0.114533 | -0.063873 | -0.062724 | -0.059204 | 0.002413 | True | True | True | -0.052389 | 0.122366 |
Step 7: Generate Table 1#
Pipeline step that generated this data
table1_overnight_full_sample.csvis generated byprocess:create_table1viacreate_table1.pytable1_overnight_paper_sample.csvis generated byprocess:create_table1viacreate_table1.py
Once we have our portfolios constructed we can attempt to replicate Table 1 from the paper. Additionally, we will generate a version of the table that includes data through January 2026. The source of the portfolio data is portfolio_daily_returns.parquet as described above.
import create_table1 as ct1
ct1.main()
Wrote /home/tomhi/finmath/finm-32900/p05_lopez-lira_tang_2023/_output/table1_overnight_paper_sample.csv (rows=4)
Wrote /home/tomhi/finmath/finm-32900/p05_lopez-lira_tang_2023/_output/table1_overnight_full_sample.csv (rows=4)
Paper sample date range:
2021-10-01 00:00:00 to 2024-05-31 00:00:00
Full sample date range:
2021-10-01 00:00:00 to 2026-01-30 00:00:00
full_sample_table = pd.read_csv(paths["Table1 (Oct 2021 - March 2026)"]) if paths["Table1 (Oct 2021 - March 2026)"].exists() else pd.DataFrame()
paper_sample_table = pd.read_csv(paths["Table1 (Oct 2021 - May 2024)"]) if paths["Table1 (Oct 2021 - May 2024)"].exists() else pd.DataFrame()
display(printable_table(full_sample_table.head(5), "Full Sample").style)
display(printable_table(paper_sample_table.head(5), "Paper Sample Replication").style)
Full Sample Data: (4, 8)
Summary:
> Trading Days: 1087
> Firm-Day Observations: 176968
| Portfolio | Initial Reaction Hit Rate (%) | Initial Reaction Mean Return (%) | Drift Hit Rate (%) | Drift Mean Return (%) | Drift Sharpe Ratio | |
|---|---|---|---|---|---|---|
| 0 | Long-Short Portfolio | 93.100000 | 4.381000 | 51.702000 | 0.165000 | 0.912000 |
| 1 | Long-Only Portfolio | 85.465000 | 3.004000 | 47.470000 | -0.036000 | -0.321000 |
| 2 | Short-Only Portfolio | 84.885000 | 1.392000 | 55.945000 | 0.187000 | 0.993000 |
Paper Sample Replication Data: (4, 8)
Summary:
> Trading Days: 670
> Firm-Day Observations: 112092
| Portfolio | Initial Reaction Hit Rate (%) | Initial Reaction Mean Return (%) | Drift Hit Rate (%) | Drift Mean Return (%) | Drift Sharpe Ratio | |
|---|---|---|---|---|---|---|
| 0 | Long-Short Portfolio | 92.836000 | 3.300000 | 52.239000 | 0.240000 | 1.458000 |
| 1 | Long-Only Portfolio | 84.179000 | 1.973000 | 48.507000 | 0.020000 | 0.195000 |
| 2 | Short-Only Portfolio | 85.629000 | 1.353000 | 54.491000 | 0.215000 | 1.214000 |
Replication Comparison#
actual_paper_table = pd.read_csv(paths["Paper Table Data"]) if paths["Paper Table Data"].exists() else pd.DataFrame()
display(printable_table(actual_paper_table, "Actual Paper Table").style)
Actual Paper Table Data: (4, 8)
Summary:
> Trading Days: 670.0
> Firm-Day Observations: 105742
| Portfolio | Initial Reaction Hit Rate (%) | Initial Reaction Mean Return (%) | Drift Hit Rate (%) | Drift Mean Return (%) | Drift Sharpe Ratio | |
|---|---|---|---|---|---|---|
| 0 | Long-Short Portfolio | 93.280000 | 3.060000 | 58.060000 | 0.340000 | 2.970000 |
| 1 | Long-Only Portfolio | 83.280000 | 1.270000 | 50.900000 | 0.080000 | 0.780000 |
| 2 | Short-Only Portfolio | 79.400000 | 1.790000 | 53.580000 | 0.260000 | 2.010000 |
actual = actual_paper_table
compare_cols = [col for col in paper_sample_table.columns if col in actual.columns]
actual_cmp = actual[compare_cols].set_index("Portfolio")
paper_cmp = paper_sample_table[compare_cols].set_index("Portfolio")
numeric_cols = [col for col in compare_cols if col != "Portfolio"]
diff_table = paper_cmp[numeric_cols].apply(pd.to_numeric, errors="coerce") - actual_cmp[
numeric_cols
].apply(pd.to_numeric, errors="coerce")
display(diff_table.reset_index().style.format({col: "{:.4f}" for col in numeric_cols}))
pct_error_table = pd.DataFrame(index=actual_cmp.index)
for col in numeric_cols:
actual_vals = pd.to_numeric(actual_cmp[col], errors="coerce")
paper_vals = pd.to_numeric(paper_cmp[col], errors="coerce")
pct_error = ((paper_vals - actual_vals) / actual_vals.replace(0, pd.NA)) * 100
pct_error_table[f"{col} | actual"] = actual_vals
pct_error_table[f"{col} | paper_sample"] = paper_vals
pct_error_table[f"{col} | error (%)"] = pct_error
pct_formats = {}
for col in numeric_cols:
pct_formats[f"{col} | actual"] = "{:.4f}"
pct_formats[f"{col} | paper_sample"] = "{:.4f}"
pct_formats[f"{col} | error (%)"] = "{:.2f}%"
error_cols = [c for c in pct_error_table.columns if c.endswith("| error (%)")]
display(
pct_error_table[error_cols]
.reset_index()
.style.format({col: "{:.2f}%" for col in error_cols})
)
| Portfolio | Initial Reaction Hit Rate (%) | Initial Reaction Mean Return (%) | Drift Hit Rate (%) | Drift Mean Return (%) | Drift Sharpe Ratio | Trading Days | Firm-Day Observations | |
|---|---|---|---|---|---|---|---|---|
| 0 | Long-Short Portfolio | -0.4440 | 0.2400 | -5.8210 | -0.1000 | -1.5120 | nan | nan |
| 1 | Long-Only Portfolio | 0.8990 | 0.7030 | -2.3930 | -0.0600 | -0.5850 | nan | nan |
| 2 | Short-Only Portfolio | 6.2290 | -0.4370 | 0.9110 | -0.0450 | -0.7960 | nan | nan |
| 3 | Sample Summary | nan | nan | nan | nan | nan | 0.0000 | 6350.0000 |
| Portfolio | Initial Reaction Hit Rate (%) | error (%) | Initial Reaction Mean Return (%) | error (%) | Drift Hit Rate (%) | error (%) | Drift Mean Return (%) | error (%) | Drift Sharpe Ratio | error (%) | Trading Days | error (%) | Firm-Day Observations | error (%) | |
|---|---|---|---|---|---|---|---|---|
| 0 | Long-Short Portfolio | -0.48% | 7.84% | -10.03% | -29.41% | -50.91% | nan% | nan% |
| 1 | Long-Only Portfolio | 1.08% | 55.35% | -4.70% | -75.00% | -75.00% | nan% | nan% |
| 2 | Short-Only Portfolio | 7.85% | -24.41% | 1.70% | -17.31% | -39.60% | nan% | nan% |
| 3 | Sample Summary | nan% | nan% | nan% | nan% | nan% | 0.00% | 6.01% |
Comparison Discussion#
As we can see from the above, some of our replication was fairly consistent with the papers results, specifically Initial Reaction Hit Rate of the Long-Short and Long-Only portfolios as well as the Drift Hit Rate of the Short-Only portfolio. Unfortuantely, everything else was substantially different.
This can almost certainly be attributed to one key fact: the GPT model specified in the paper, gpt-4-0314, was deprecated by OpenAI and no logner available. Therefore we opted to use the next most recent model gpt-3.5-turbo. Given the paperās results suggesting the older the model the worse it would perform, it is no surprise that the mdoel we used largely performed worse.
Specifically, we note that our responses from OpenAI using the older model produced significantly more UNKNOWN responses than the papers model, i.e. ~67% vs 34.7%
import create_openai_responses_table as cort
cort.main()
label proportion_2021_2024_sample_period proportion_full_sample count_2021_2024_sample_period count_full_sample
YES 0.268041 0.272443 34874 57134
NO 0.060035 0.062133 7811 13030
UNKNOWN 0.671924 0.665424 87422 139546
Wrote /home/tomhi/finmath/finm-32900/p05_lopez-lira_tang_2023/_output/openai_output_label_proportions.csv