I've been building an automated swing-trading system for NSE stocks — paper trading only, running
daily on GitHub Actions. A few weeks ago I decided to add a news sentiment layer, and my first
instinct was to reach for an off-the-shelf tool like VADER or TextBlob. I'm glad I didn't.
The problem with generic sentiment lexicons in finance
In 2011, Loughran and McDonald published a paper in the Journal of Finance that's since become
foundational in financial NLP. They took the Harvard-IV General Inquirer — a standard,
widely-used sentiment dictionary at the time — and checked how well its "negative" word list
actually held up against real 10-K filings.
About three-quarters of the words it flagged as negative weren't negative at all in a financial
context. Words like "liability," "tax," "cost," "capital," even "president" (as in "vice
president") read as bad news in everyday English and are completely routine vocabulary in
business writing. A generic model scoring financial headlines is, in a very literal sense,
scoring the wrong language.
So instead of importing a library, I built a small sentiment engine from the ground up, following
the LM methodology: separate word categories for Negative, Positive, Uncertainty, and — the one
that turned out to matter most — Litigious.
Two free sources, on purpose
Headlines come from two independent free sources: Yahoo Finance's news feed (via yfinance) and
Google News RSS. No API keys, no paid tier. Two sources isn't redundancy for its own sake — I'd
already been burned once elsewhere in this project by depending on a single free data source that
silently changed its response shape. If one schema shifts without notice, the other source still
covers you.
The negation bug that taught me the most
Early testing threw a sentence at the scorer that should obviously read negative:
"Quarterly results were not impressive, margins weak."
It came back positive. The bug: my negation check looked backward a few words from any
sentiment-bearing term to see if a negator preceded it. "Not" was technically within that window
of "weak" — it just belonged to a different clause, negating "impressive," not "weak" at all.
The fix was scoping negation to stop at clause boundaries (commas, "but," "however") instead of
just counting tokens. A small thing, but it's exactly the kind of bug that looks fine on
obviously-easy test sentences and quietly wrecks real headlines.
Why one dramatic headline shouldn't move the needle
A single very negative headline about a stock with otherwise thin news coverage gets
shrunk toward neutral before it's trusted — the same statistical device (empirical-Bayes
shrinkage toward a prior) used elsewhere in the project to stop a small sample of trades from
over-swinging a pattern's calibrated weight. A cluster of five corroborating headlines gets
trusted much more than one. Headlines also decay in relevance — today's news outweighs a
five-day-old story, on an exponential half-life, so stale coverage doesn't linger in the score
forever.
Two different jobs, kept separate
The sentiment score does two things, and I deliberately didn't collapse them into one number:
- A soft factor — a small (10%-weighted) input into a larger cross-sectional conviction score that also includes momentum, trend quality, volume, and relative strength. Mildly positive news nudges conviction up a little. It's a weak, noisy signal next to price/volume factors with decades of published evidence behind them, and it's sized accordingly.
- A hard veto — an independent, absolute-threshold check that can block a trade outright on a fresh, corroborated cluster of litigious/fraud-flagged headlines, regardless of how good everything else looks. This is the asymmetric case a pure price-based system is structurally blind to until the news is already priced into the chart.
Where this fits into the bigger picture
The sentiment engine is one of five layers a trade candidate has to clear: a technical pattern
scanner, a fundamental soft-gate, a cross-sectional alpha score, this sentiment layer, and a
multi-tier risk manager (portfolio risk budget, sector concentration caps, a drawdown circuit
breaker). Every layer can be independently toggled on or off, specifically so I can A/B test
whether each one is actually contributing anything, instead of just trusting that a
sophisticated-sounding feature is a helpful one.
I'm not going to claim this makes money — the live paper-trading sample is still small, and I'd
rather build the validation tooling properly than dress up a handful of trades as a track record.
That's genuinely the more interesting engineering problem anyway: not "does this sound smart," but
"how do I actually find out."
Repo (fully open, MIT licensed): https://github.com/tanmaykaper/Paper-Trading-Bot
Would genuinely welcome pushback on the lexicon design or the shrinkage constants — still tuning
both.
Top comments (0)