Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion dashboard/backend/integrations/news_sentiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,9 @@ def _story_fields(sig: dict) -> dict:
def _project_entry(sig: dict, reference_ts: float) -> dict:
return {
"sentiment": sig["sentiment"],
"score": sig["score"],
# v2 sentiment_score with transitional v1 fallback — PR-2 of the
# FinSearch score-field disambiguation deletes the fallback.
"score": sig["sentiment_score"] if "sentiment_score" in sig else sig["score"],
**_story_fields(sig),
"n_articles": sig["n_articles"],
"age_hours": max(0.0, (reference_ts - float(sig["published"])) / 3600.0),
Expand Down
2 changes: 1 addition & 1 deletion dashboard/backend/tests/test_news_sentiment_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def test_items_fixture_is_the_batch_the_signals_fixture_came_from():

def test_fixture_matches_contract_essentials():
body = load_signals_fixture()
assert body["schema_version"] == 1
assert body["schema_version"] in (1, 2) # transitional; PR-2 pins == 2
assert isinstance(body["signals"], dict) and body["signals"]
sample = next(iter(body["signals"].values()))
for field in ("sentiment", "score", "rationale", "headline", "source",
Expand Down
36 changes: 36 additions & 0 deletions dashboard/backend/tests/test_sentiment_score_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""PR-1 of the FinSearch score-field disambiguation (see FinSearch spec
2026-07-14-score-field-disambiguation-design.md): _project_entry must read
sentiment_score (signals v2) and fall back to score (v1) until PR-2 deletes
the fallback."""
import pytest

from dashboard.backend.integrations.news_sentiment import _project_entry

BASE = {"sentiment": "bullish", "rationale": "r", "headline": "h",
"source": "Reuters", "url": "https://example.com/a",
"published": 1783330000.0, "guid": "g1", "n_articles": 2}


def test_project_entry_reads_sentiment_score_v2():
entry = _project_entry({**BASE, "sentiment_score": 0.5},
reference_ts=1783333600.0)
assert entry["score"] == 0.5


def test_project_entry_falls_back_to_v1_score():
entry = _project_entry({**BASE, "score": -0.3},
reference_ts=1783333600.0)
assert entry["score"] == -0.3


def test_project_entry_prefers_sentiment_score_when_both_present():
entry = _project_entry({**BASE, "sentiment_score": 0.5, "score": -0.9},
reference_ts=1783333600.0)
assert entry["score"] == 0.5


def test_project_entry_raises_when_both_score_keys_absent():
# Fail loud like every other required field: a payload with neither key is
# broken upstream, and score=None would flow silently into the panel.
with pytest.raises(KeyError):
_project_entry(dict(BASE), reference_ts=1783333600.0)
11 changes: 8 additions & 3 deletions docs/integrations/finsearch-news-sentiment.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ No ATL-side contract change is needed — the `/api/v2` refactor shipped the who
| `age_hours` | float | ≥ 0.0 |
| `n_articles` | int | ≥ 0 |
| `rationale` | str \| None | optional — see design note |

> **Transitional:** until FinSearch's schema-v2 deploy lands, the producer
> still sends `score` (v1); the adapter reads `sentiment_score` with a `score`
> fallback. PR-2 removes the fallback and pins `schema_version == 2`.

- **Universe:** the loader is called with `list(DJIA_30)` (the canonical current Dow-30 constant, `infrastructure/llm/validator.py`, reconciled in #91/#94 — the old `AMEX` typo is gone). Pass that straight through as the `?tickers=` filter.

---
Expand Down Expand Up @@ -82,7 +87,7 @@ Top level — the fields the **public, bearer-gated** response carries (consume
```json
"MSFT": {
"sentiment": "bullish",
"score": 0.5,
"sentiment_score": 0.5,
"rationale": "Two distinct outlets report upbeat Azure guidance.",
"headline": "Microsoft raises Azure guidance after record quarter",
"source": "Reuters",
Expand All @@ -104,7 +109,7 @@ The producer artifact and the consumer type were designed independently and **do
| `NewsSentimentEntry` | ← source | transform |
|----------------------|----------|-----------|
| `sentiment` | `sentiment` | passthrough — the enums are identical (`bullish`/`bearish`/`neutral`) |
| `score` | `score` | passthrough (already −1…1) |
| `score` | `sentiment_score` | passthrough (already −1…1) |
| `headline` | `headline` | passthrough |
| `source` | `source` | passthrough |
| `url` | `url` | passthrough |
Expand Down Expand Up @@ -191,7 +196,7 @@ def get_news_sentiment(universe, timestamp):
for ticker, s in body.get("signals", {}).items():
out[ticker] = {
"sentiment": s["sentiment"],
"score": s["score"],
"score": s["sentiment_score"],
"headline": s["headline"],
"source": s["source"],
"url": s["url"],
Expand Down
Loading