← PROTOTYPES

Sentiment · how every reading is built

23 September 2026 · branch hub/sentiment-3-20260923 · built and checked on the MacBook · nothing deployed, nothing pushed, nothing written to the database

You said four things. "The fear and greed gauge the normal one, how it's expressed normally, is better than just putting 35." — the gauges are now drawn the way their publishers draw them, in colour, with the previous close, week and month beside them. "I need to see how the sentiment is being built from the first scintillas that are measured upwards." — there is a new page, HOW IT IS BUILT, that walks from the number down to the words in the actual post, and a keyword tracker that says which words fired today. "No individual headline carries a stored sentiment score, so how are you making a news sentiment?" — you were right, and it is fixed: every headline is now scored here, with a finance dictionary you can read, and the words that fired are kept. "You can remove StockTwits." — gone from the room and from every blend.

1 · The dial, drawn the way CNN draws it

The sentiment room at 1680 wide
The room at 1,680 wide, read live at 20:45Z. CNN's own number, 35 · fear, on a half dial carrying the five zones in colour — red extreme fear on the left through green extreme greed on the right — with the needle where 35 falls. Underneath: previous close 35 (orange, fear), one week ago 27 (red, extreme fear), one month ago 55 (yellow-green), one year ago 57. Then CNN's seven parts, each coloured by its own zone: 52-week highs vs lows and advancing volume both at 0 · extreme fear, safe haven demand at 59 · greed. Beside it the crypto gauge, 71 · Greed, with 78 / 51 / 73 behind it. On the right the YouTube voice: 358 channels, 736 clips, neutral 53, and the channels disagree (43% on the smaller side).

Nothing on these cards is ours. The number, the word beside it, the date and the three comparisons are the publisher's own, taken from the same feed their page uses. The zones are CNN's: under 25 extreme fear, 25–45 fear, 45–55 neutral, 55–75 greed, over 75 extreme greed.

VIX is now the live quote, not yesterday's close. It comes from the chart API's /macro, which carries a quote with its own observation time — at the time of writing 15.18, against a previous close of 14.21, observed 20:09Z. The card says which it is showing. The one-month against three-month shape still cannot be drawn: the chart API does not carry VIX3M and Cboe's own file refuses a browser, and the card says so rather than leaving a blank.

2 · How it is built — the ladder down to the words

The HOW IT IS BUILT page with a channel and its clips opened
The new page, with two rungs opened. ALL OF YOUTUBE · neutral 53 — 358 channels, 745 clips, 7 days. Opened, it lists the channels; StockInvest.us · −0.64 is open here, showing its 22 clips across 8 tickers. Each clip is the text that was actually measured, with Rally marked green where it counted bullish and fall marked red where it counted bearish, and the arithmetic underneath: +1 bullish · −1 bearish → 0. On the right, the keyword tracker: bullish 4 / 38 means the word fired 4 times in the last 24 hours and 38 times in the last 7 days; shorts 11 / 93 on the bearish side. Below it, the complete word list, every word the social readings can match.

Every level is a row you can open, and the bottom level is the sentence itself. The leans on that page are not a second opinion: they are the same arithmetic the room's numbers are made of, so opening a row can only show you the parts of the number above it. A post is +1 for each bullish word and −1 for each bearish one; a handle or channel is the average of the signs of its items, so one prolific account cannot carry a reading; the voice is the average of the handles.

A negator flips a word, and the page shows it. "Not strong" is scored as a negative and drawn struck through, with the reason on hover. That rule is three words wide — a negator within the three words before a match — which is what the job has always done and is now visible.

3 · News, read properly

You put your finger on the real problem: "The amount of headlines is not bullish or bearish until a transcript is reviewed… no individual headline carries a stored sentiment score, so how are you making a news sentiment?" The honest answer was that the room printed a number from a table called news_sentiment, written by a job that is not in this repository. Nobody could say which words, which window or which weighting produced it.

What it is now. Every headline is scored here, in front of you, against a finance-specific dictionary — the Loughran–McDonald word list, which was built for financial text rather than for film reviews: 354 positive words and 2,355 negative ones. That imbalance matters, so a headline's score is the balance of the words that fired — positive minus negative, divided by how many fired — never a raw count. The dictionary was written from 10-K filings, so it does not carry the verbs a headline actually uses: beats, surges, plunges, rally, soar, tumble, selloff, upgrade are all missing from it, which is why "NVDA beats and surges" scored neutral on the dictionary alone. 67 of those price verbs were added, and every word that fires is stored with which list it came from, so you can always see which part did the work.

Measured on 2,000 real headlines, this evening

whatcountwhat it means
headlines read (last 24 hours)2,000of 4,583 stored in the window
carried a scoring word914these are the only ones that count
carried no scoring word at all1,086they get no score — not a zero. This is your point, measured: more than half of a day's headlines say nothing either way
asked a question241scored and shown, then left out of the total: a question is not a claim
flipped by a negator14"not strong", "no downgrade"
the reading+0.247the average of the 914, each counting once

The words that fired most: better 116, gains 81, strong 79, rally 78, breakthrough 58, surges 53, despite 45, error 41, slides 39, bullish 39. Two of those are worth your attention. "despite" is in the dictionary's positive list — that is Loughran–McDonald's own classification, and it is visibly wrong for a headline like "shares fall despite the beat". You can only see that because every word is stored. And 1,644 of the hits came from the dictionary against 555 from the added price verbs, so the finance dictionary is doing most of the work, not our additions.

Three real examples from tonight, exactly as scored:

Could a model read the article instead, without a paid API?

Not honestly, not today. I looked at it properly, and here is the answer in plain words. A model that reads the whole article needs either a paid API — which you have ruled out — or a model running on this Mac. A local model (Ollama or llama.cpp with a small open model) is genuinely free and private, and would handle the language a word list cannot: irony, hedging, "beats but guides lower". But: nothing of the sort is installed on this machine; 4,583 headlines a day is roughly 4,500 model calls, which on a laptop is hours of work and a hot machine, and it competes with everything else you run; the articles themselves are not stored, only the headline and a snippet, so it would need to fetch each page first; and a model's reading cannot be taken apart the way a word list can — you would be back to a number nobody can check. My recommendation: keep the transparent word list as the stored, checkable reading, and treat a local model as a later second opinion on the few hundred headlines that matter (your own holdings), where its disagreement with the word list is itself the signal. I did not install anything and did not call any model.

What is stored, and what root must run

The scoring you see in the room happens in your browser, live, because the table does not exist yet. The migration and the job are written and tested and are not applied — that is the coordinator's step:

The old news_sentiment table is untouched. It is still shown on the NEWS page, labelled for what it is: a number whose method nobody can state.

The news page, every headline scored
The NEWS page, live in the browser at 20:47Z. 4,579 headlines in the last 24 hours, 742 carried a scoring word, 910 had none and got no score, 348 asked a question and were left out. The reading: +0.192 on a scale of −1 to +1, which is 60 of 100 · greed, with the window and the weighting written out underneath and the coverage stated plainly — the browser read the newest 2,000 of 4,579. Below that the ticker ladder: IONQ +0.39 from 37 scored headlines, MCD −0.15 from 27, each one opening into the headlines and the words that scored them. The right-hand column names the dictionary, explains the balance rule, and says exactly what root must run to store the rows.

These counts differ slightly from the 2,000-headline table above because the page and the job were read twenty minutes apart and count the leftovers differently: the page reports questions separately from "had none", the job's table above counts them inside its own totals. Both read the same 2,000 newest headlines with the same dictionary.

4 · StockTwits is gone

Removed from the sentiment room, from the voices list and from every blend. Its ingester stopped writing months ago and the row was never counted, so it was one more dead thing to explain. The stored messages in social_posts are left exactly where they are — nothing was deleted from the database. The one place it still appears is the older SOCIAL room's reserved tab, which belongs to another lane and is protected by that lane's own test; removing it there is decision 1 below.

5 · Keeping the YouTube reading alive

Its scheduled job is still not installed, and I installed nothing. jobs/youtube-sentiment/SCHEDULING.md is the exact thing to run, and it names a problem nobody had noticed: the plist points at lane M20's worktree, a temporary folder that is deleted when that branch is merged. Scheduled as it stands, the job would quietly fail every half hour. It must point at the deployed checkout first.

The job itself now does what lane M20 recommended: it writes one row per video and ticker into youtube_video_sentiment, so the blend can cover every ticker the channels talked about instead of the nine the pooled roll-up happens to carry. Each row keeps the bullish and bearish hit counts, the words that fired, and the ±40-word transcript window the lean came from — which is what will let the room show you the sentences a YouTube reading is made of, exactly as it now shows you the words in a post. The numbers it already produces are unchanged: the counting function was split in two so it hands back the words, and the arithmetic is the sum of those words.

6 · On the phone

The room at 390 wide
390 wide, as the room first appears. The cards stack one per row and the dial keeps its five zones and its needle at 340px wide. The panel scrolls, the way every panel on the phone does, so the card's lower half sits below the fold — the next picture is the same card scrolled.
The same phone card, scrolled
The same card, scrolled down. The previous readings wrap into two rows of two — previous close 35 and one week ago 27, then one month ago 55 and one year ago 57 — each in its own zone colour, and CNN's seven parts follow with theirs. I measured the page as well as looking at it: the card is 340px wide inside a 390px screen and there is no sideways scroll.
How it is built, at 390 wide
The ladder at 390 wide, with the top rung opened: the channels list, each with its own lean, and the keyword tracker below in one column instead of two.

7 · What could be wrong

8 · What I did not do

9 · Three decisions for you

1 · The dead StockTwits tab in the older SOCIAL room. It still promises "message volume and bull/bear ratio light up here when the ingester lands", which is not going to happen. I left it because that room belongs to another lane and its test pins it. My recommendation: let me remove it in the next lane, in one line.
2 · Which window the news reading uses. It is 24 hours now, so it answers "what is the news saying today". The stored rows would make 3 or 7 days just as cheap, which is steadier but slower to turn. My recommendation: keep 24 hours as the headline number and show a 7-day line beside it once the rows are stored.
3 · Whether a local model gets a trial. It is free and private but slow, and its reading cannot be taken apart. My recommendation: not yet. Let the stored per-headline rows run for a week first — they will show you how often the word list is obviously wrong, and that is the number that decides whether a model is worth the trouble.

Branch hub/sentiment-3-20260923. Tests: node --test tests/ — 439 tests, 2 failures, both of which already fail on production (every place this surface prints an age uses the one function and xfeed-publish). 12 new tests in tests/sentiment-anatomy.test.mjs, and the browser, the job and those tests are checked against the same fixtures so the three cannot drift apart.