# Market Sentiment Analysis with NLP

A working, AI-assisted portfolio research project for Haashir Ahmed, implemented in September 2026. It compares VADER and TextBlob scores on historical financial headlines with subsequent 30-calendar-day adjusted-price returns. The website displays computed results; it is not a live prediction service.

## What this implementation found

The sample contains 1,433 raw Agilent (ticker A) headline records dated April 29, 2009–June 5, 2020. Removing 16 duplicate records leaves **1,417 headlines**, covering **467 entry-date windows**. VADER and TextBlob agree on the displayed positive/neutral/negative label for **64.64%** of headlines. Agreement is not a measure of classification accuracy; this sample has no human sentiment labels.

The evaluation uses 68 non-overlapping training windows and 31 non-overlapping held-out windows. Two windows crossing the training/test boundary are purged. The remaining overlapping windows are available for inspection in the CSV, but are not used to fit or evaluate the regression models.

| Predictor | Holdout MAE (percentage points) | Holdout RMSE (percentage points) | Direction accuracy |
| --- | ---: | ---: | ---: |
| Constant training mean | 4.8052 | 6.0046 | 74.19% |
| VADER + linear regression | 4.9743 | 6.1037 | 70.97% |
| TextBlob + linear regression | 5.2464 | 6.2424 | 67.74% |

Neither sentiment model beats the constant baseline on mean absolute error in this holdout. VADER's holdout Pearson correlation is -0.0872; TextBlob's is -0.1100. This is a small, exploratory study of one stock. No statistical significance, market-wide predictive accuracy, causal effect, or profitable trading strategy is claimed. An always-up forecast is correct on 74.19% of the holdout windows, illustrating why directional accuracy needs a baseline.

## Run it

Use Python 3.12 with a virtual environment. No API key is needed. Run these commands from the extracted project folder:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python fetch_sample.py
python analyze.py --news data/news.csv --prices data/prices.csv --out results
python -m unittest -v test_analysis.py
```

On Windows, activate with `.venv\Scripts\activate` instead. The code was run using Python 3.12, NumPy 2.3.5, pandas 2.2.3, vaderSentiment 3.3.2, and TextBlob 0.19.0. TextBlob's default PatternAnalyzer and VADER's bundled lexicon do not need additional corpus downloads for these operations.

The download includes precomputed results, so the analysis can also be inspected without installing Python. A network connection is required only to install packages and retrieve the original inputs. The bounded fetcher reads the first 1 MiB of the news CSV, verifies that the entire A block is present, and retrieves only A.csv from the price ZIP using HTTP ranges. It refuses a server response that ignores those ranges instead of downloading the full dataset.

## Inputs and provenance

Source: **FNSPID**, Zihan Dong, Xinyu Fan, and Zhiyuan Peng (2024).

- Paper: https://arxiv.org/abs/2402.06698
- Authors' dataset: https://huggingface.co/datasets/Zihan1004/FNSPID
- Authors' repository and release notice: https://github.com/Zdong104/FNSPID_Financial_News_Dataset
- Pinned dataset revision: `bf9189c`
- News: `Stock_news/All_external.csv`, complete first ticker `A`
- Prices: `Stock_price/full_history.zip`, member `full_history/A.csv`, using the upstream `adj close` field

Agilent was selected as the first complete ticker in the source file for a bounded, reproducible study, not because of its measured results. No news or price observations are simulated. Original timestamp accuracy, ticker assignment, and adjusted-price methodology have not been independently audited against the publishers or exchanges.

`data/provenance.json` records the original URLs, row counts, and SHA-256 hashes of the normalized input files. Raw news and price files are retrieved directly from the authors and are not bundled in the download. Public exports contain derived scores, short headline excerpts (up to 24 words), and source links; they do not contain article bodies. The underlying complete headlines are used for scoring.

The repository's LICENSE file identifies CC BY-NC 4.0. The authors' July 24, 2025 README update separately announces release for research and commercial use. Preserve source attribution and check the upstream terms for any new use; original publishers retain rights in their content. This project does not relicense the source dataset or library implementations.

## Input contracts for your own data

`news.csv` columns:

| Column | Meaning |
| --- | --- |
| published_at | ISO timestamp including explicit UTC offset or Z |
| ticker | Stock symbol matching the prices file |
| headline | Complete headline, with original casing and punctuation |
| source_url | Original article URL, or an empty string if unavailable |
| publisher | Source publication name |

`prices.csv` columns:

| Column | Meaning |
| --- | --- |
| date | Trading date, YYYY-MM-DD |
| ticker | Stock symbol |
| adj_close | Finite, positive adjusted closing price |

The pipeline rejects ambiguous duplicate ticker/date prices, missing required fields, invalid values, and publication timestamps without explicit time zones. Provide enough price history after the last headline to finish the forward windows. If a price series has gaps, it should be repaired or excluded at the source; a gap is never filled with a flat price.

## Method

1. Normalize dates to UTC days. Deduplicate the earliest occurrence of each ticker/URL and identical normalized headline within a ticker/day. Preserve the original headline for scoring.
2. Run VADER compound and TextBlob polarity/subjectivity. Display positive at >=0.05, negative at <=-0.05, and neutral in between. These are VADER's usual thresholds; applying them to TextBlob is an explicit display choice, not a calibrated financial label.
3. Map each headline to the first observed trading close strictly after its UTC publication day. This intentionally waits until the whole supplied date has passed, instead of assuming a headline was available before that day's close. Average sentiment over all headlines mapped to each ticker/entry date.
4. Set the exit to the first observed trading close on or after entry date +30 **calendar** days (not 30 trading sessions). Compute `100 × (adjusted_close_exit / adjusted_close_entry − 1)`. Refuse entry/exit matches delayed by more than seven days; exclude incomplete windows.
5. Fix the chronological holdout boundary at 2017-01-01. Training windows must finish before that date; test windows must start on or after it. Purge crossing windows.
6. Within each ticker and period, select windows greedily in date order, with each entry at or after the prior selected exit. This prevents overlapping return exposure within a ticker. The sample has only one ticker; extending it to several tickers introduces cross-sectional dependence that requires additional validation.
7. Fit separate one-feature ordinary least-squares models on the selected training windows. Compare against a constant prediction equal to the training mean return. No parameters are optimized on holdout outcomes. Predicted direction is positive versus non-positive; actual zero returns are included in the latter group.
8. Report MAE, RMSE, and directional accuracy on exactly the same held-out windows. Report descriptive Pearson correlation separately. Scores are not confidence levels or buy/sell recommendations.

The fits are intentionally simple baselines. There is no live-news integration, trading simulation, transaction-cost model, benchmark excess-return adjustment, rolling cross-validation, uncertainty interval, or finance-specific fine-tuning in this version. The next study should add more stocks and periods and compare a finance-specific model under the same chronological protocol.

## Outputs

- `analysis.json`: dashboard-ready summary, evaluation, model coefficients, all daily windows, headline excerpts, model versions, and input hashes.
- `windows.csv`: all complete aggregated windows, scores, returns, predictions, period labels, and an `evaluated` flag. Only `evaluated=True` rows count toward reported fit/evaluation results. Predictions on training rows are in-sample; predictions on test rows are out-of-sample.
- `headlines.csv`: dated excerpts, original links, sentiment scores/labels, subjectivity, and matched forward windows.

## Verification

Ten unit tests cover weekend-to-trading-day alignment, the full 30-calendar-day horizon, duplicate weighting, incomplete outcomes, missing-price gaps, timestamp zones, duplicate prices, boundary purging, per-ticker non-overlap, future-outcome invariance of the fitted coefficients, and negation in both actual sentiment models. Small invented fixtures are used only in tests; all published study results use the historical input data.

## Model references

- Hutto, C.J. & Gilbert, E.E. (2014). *VADER: A Parsimonious Rule-based Model for Sentiment Analysis of Social Media Text*. https://github.com/cjhutto/vaderSentiment
- TextBlob documentation: https://textblob.readthedocs.io/en/dev/quickstart.html#sentiment-analysis

## Project scope and authorship

This is an independent portfolio implementation created with ChatGPT assistance for Haashir Ahmed in September 2026. It is not work commissioned by Agilent, a financial institution, or a data publisher. The downloadable pipeline is intended to make its methods, assumptions, and findings reviewable and reproducible.
