newsscore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. newsscore-0.1.0/.github/workflows/ci.yml +24 -0
  2. newsscore-0.1.0/.github/workflows/publish.yml +46 -0
  3. newsscore-0.1.0/.gitignore +8 -0
  4. newsscore-0.1.0/CONTRIBUTING.md +50 -0
  5. newsscore-0.1.0/LICENSE +21 -0
  6. newsscore-0.1.0/PKG-INFO +365 -0
  7. newsscore-0.1.0/README.md +338 -0
  8. newsscore-0.1.0/pyproject.toml +55 -0
  9. newsscore-0.1.0/src/newsscore/__init__.py +42 -0
  10. newsscore-0.1.0/src/newsscore/_version.py +1 -0
  11. newsscore-0.1.0/src/newsscore/aggregate.py +78 -0
  12. newsscore-0.1.0/src/newsscore/cache.py +99 -0
  13. newsscore-0.1.0/src/newsscore/cli.py +312 -0
  14. newsscore-0.1.0/src/newsscore/config.py +151 -0
  15. newsscore-0.1.0/src/newsscore/http.py +21 -0
  16. newsscore-0.1.0/src/newsscore/models.py +163 -0
  17. newsscore-0.1.0/src/newsscore/scorer.py +299 -0
  18. newsscore-0.1.0/src/newsscore/scoring/__init__.py +56 -0
  19. newsscore-0.1.0/src/newsscore/scoring/jev.py +173 -0
  20. newsscore-0.1.0/src/newsscore/scoring/keyword.py +94 -0
  21. newsscore-0.1.0/src/newsscore/scoring/protocol.py +119 -0
  22. newsscore-0.1.0/src/newsscore/sources/__init__.py +68 -0
  23. newsscore-0.1.0/src/newsscore/sources/alpha_vantage.py +55 -0
  24. newsscore-0.1.0/src/newsscore/sources/base.py +154 -0
  25. newsscore-0.1.0/src/newsscore/sources/finnhub.py +43 -0
  26. newsscore-0.1.0/src/newsscore/sources/marketaux.py +54 -0
  27. newsscore-0.1.0/src/newsscore/sources/newsapi.py +59 -0
  28. newsscore-0.1.0/src/newsscore/sources/polygon.py +71 -0
  29. newsscore-0.1.0/src/newsscore/sources/rss.py +103 -0
  30. newsscore-0.1.0/src/newsscore/sources/tiingo.py +45 -0
  31. newsscore-0.1.0/tests/conftest.py +51 -0
  32. newsscore-0.1.0/tests/test_config.py +57 -0
  33. newsscore-0.1.0/tests/test_core.py +90 -0
  34. newsscore-0.1.0/tests/test_jev.py +69 -0
  35. newsscore-0.1.0/tests/test_scorer.py +139 -0
  36. newsscore-0.1.0/tests/test_sources.py +164 -0
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest]
15
+ python: ["3.10", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - run: uv sync
22
+ - run: uv run pytest -q
23
+ - run: uv build
24
+ - run: uvx twine check dist/*
@@ -0,0 +1,46 @@
1
+ # Publishes to PyPI via Trusted Publishing (OIDC): no API token is stored anywhere.
2
+ # One-time setup on pypi.org -> Your account -> Publishing -> "Add a new pending publisher":
3
+ # PyPI project name: newsscore
4
+ # Owner: mahynotch Repository: newsscore
5
+ # Workflow name: publish.yml Environment name: pypi
6
+ # Then: git tag v0.1.0 && git push origin v0.1.0 (or create a GitHub Release).
7
+ name: Publish to PyPI
8
+
9
+ on:
10
+ push:
11
+ tags: ["v*"]
12
+ workflow_dispatch:
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v5
20
+ - run: uv sync
21
+ - run: uv run pytest -q
22
+ - run: uv build
23
+ - run: uvx twine check dist/*
24
+ - name: Check tag matches package version
25
+ if: startsWith(github.ref, 'refs/tags/v')
26
+ run: |
27
+ tag="${GITHUB_REF_NAME#v}"
28
+ ver="$(uv run python -c 'import newsscore; print(newsscore.__version__)')"
29
+ test "$tag" = "$ver" || { echo "tag $tag != version $ver"; exit 1; }
30
+ - uses: actions/upload-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ publish:
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment: pypi
39
+ permissions:
40
+ id-token: write # required for Trusted Publishing
41
+ steps:
42
+ - uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
@@ -0,0 +1,50 @@
1
+ # Contributing
2
+
3
+ Thanks for looking at newsscore. Here is the short version.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/mahynotch/newsscore.git && cd newsscore
9
+ uv sync # venv with the package, the Jev SDK and test tools
10
+ cp .env.example .env # fill in whatever keys you have; blanks are ignored
11
+ uv run newsscore doctor
12
+ uv run pytest
13
+ ```
14
+
15
+ ## Most wanted: live testers
16
+
17
+ Several sources have only been tested against fixture payloads. If you have a key
18
+ for Alpha Vantage, Marketaux, Tiingo (news-enabled plan) or a non-Yahoo RSS/Atom
19
+ feed, please run:
20
+
21
+ ```bash
22
+ uv run newsscore source add <type>
23
+ uv run newsscore -v fetch AAPL -s <type> -d 7
24
+ uv run newsscore score AAPL -s <type> --scorer keyword -a 5
25
+ ```
26
+
27
+ and open an issue with the provider, your plan tier, and the output (redact the key).
28
+ "Works as expected" is as valuable as a bug report.
29
+
30
+ ## Pull requests
31
+
32
+ 1. One topic per PR. Small is good.
33
+ 2. Add or update a test. Sources get a fixture test in `tests/test_sources.py`;
34
+ scorers and engine changes go in `tests/test_core.py` or `tests/test_scorer.py`.
35
+ 3. `uv run pytest` must pass.
36
+ 4. Update the README (sources table, scorers table, or the live-test status table)
37
+ when behaviour or coverage changes.
38
+ 5. Never commit `.env`, API keys, or cache files. `.gitignore` covers the usual paths;
39
+ `git diff --cached` before committing is a good habit.
40
+
41
+ ## Style
42
+
43
+ Plain Python 3.10+, type hints, dataclasses, docstrings that say *why*. No new
44
+ runtime dependencies without a reason in the PR description. Async for anything
45
+ that touches the network; sync wrappers stay thin.
46
+
47
+ ## Reporting bugs
48
+
49
+ Include the command or code, the full error, the provider and plan if a source is
50
+ involved, and `uv run newsscore --version`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Huaiyuan Ma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,365 @@
1
+ Metadata-Version: 2.5
2
+ Name: newsscore
3
+ Version: 0.1.0
4
+ Summary: News sentiment scoring for stocks. Mainstream news APIs in, a pluggable scorer (TypeSafe Jev by default), one score per symbol out.
5
+ Project-URL: Homepage, https://github.com/mahynotch/newsscore
6
+ Project-URL: Repository, https://github.com/mahynotch/newsscore
7
+ Project-URL: Issues, https://github.com/mahynotch/newsscore/issues
8
+ Author-email: Huaiyuan Ma <huaiyuanma2003@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: finance,jev,news,quant,sentiment,typesafe
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: platformdirs>=4
19
+ Requires-Dist: typer>=0.12
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
22
+ Requires-Dist: pytest>=8; extra == 'dev'
23
+ Requires-Dist: respx>=0.21; extra == 'dev'
24
+ Provides-Extra: jev
25
+ Requires-Dist: typesafe-sdk>=0.6; extra == 'jev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # newsscore
29
+
30
+ News sentiment scoring for stocks, powered by TypeSafe **Jev** by default. Mainstream financial news APIs go in, a pluggable
31
+ scoring function (TypeSafe's **Jev** by default) scores every article, and one
32
+ aggregate number per symbol comes out. Use it from Python (sync or async) or from the
33
+ `newsscore` command line.
34
+
35
+ ```
36
+ sources ──► fetch (async, concurrent) ──► de-dupe ──► score_fn (batched, cached) ──► aggregate ──► ScoreResult
37
+ ```
38
+
39
+ ## Install
40
+
41
+ Not on PyPI yet. Install straight from GitHub, or from a clone if you want to hack on it.
42
+
43
+ ```bash
44
+ # from GitHub
45
+ pip install "newsscore[jev] @ git+https://github.com/mahynotch/newsscore.git"
46
+ pip install "git+https://github.com/mahynotch/newsscore.git" # without the Jev scorer
47
+ pip install "newsscore[jev] @ git+https://github.com/mahynotch/newsscore.git@main" # pin a branch, tag or commit
48
+
49
+ # from a clone
50
+ git clone https://github.com/mahynotch/newsscore.git && cd newsscore
51
+ pip install -e ".[jev,dev]" # editable, with the Jev SDK and the test tools
52
+ ```
53
+
54
+ Python 3.10+. Runtime dependencies: `httpx`, `typer`, `platformdirs`.
55
+
56
+ ### Do I need the `[jev]` extra?
57
+
58
+ Only for the default scorer. `[jev]` adds one package, `typesafe-sdk`, and `JevScorer` is
59
+ the only thing that imports it — sources, aggregation, caching, the CLI and your own
60
+ `score_fn` all work without it.
61
+
62
+ | you want | install | also needed |
63
+ |---|---|---|
64
+ | Jev scoring (the point of this package) | `[jev]` | `TYPESAFE_API_KEY` |
65
+ | your own model as `score_fn` | plain | nothing |
66
+ | a look at the plumbing, offline | plain | nothing — you get the `keyword` scorer |
67
+
68
+ `NewsScorer()` uses Jev when the SDK is importable **and** `TYPESAFE_API_KEY` is set, and
69
+ otherwise falls back to the `keyword` lexicon scorer with only a log line. That fallback is
70
+ a test double, not a trading signal, so run `newsscore doctor` to see which one you have
71
+ before trusting a number.
72
+
73
+ ## API keys: the `.env` file
74
+
75
+ Copy `.env.example` to `.env` in the project (or working) directory and fill in the
76
+ keys you have:
77
+
78
+ ```
79
+ TYPESAFE_API_KEY=... # Jev scorer
80
+ FINNHUB_API_KEY=...
81
+ POLYGON_API_KEY=...
82
+ ```
83
+
84
+ `newsscore` and `NewsScorer.from_config()` load it automatically. Real environment
85
+ variables always win, blank values are ignored, and the file is git-ignored. Lookup:
86
+ `$NEWSSCORE_ENV` alone if set, otherwise `./.env`, then `.env` in the user config directory. In code you
87
+ can also call `load_env("path/to/.env")` yourself before constructing sources.
88
+
89
+ ## Quick start: CLI
90
+
91
+ ```bash
92
+ # 1. save some sources (stored in a per-user JSON file, see `newsscore config-path`)
93
+ newsscore source add yahoo # keyless RSS, works immediately
94
+ newsscore source add finnhub --api-key YOUR_KEY
95
+ newsscore source add polygon # key taken from $POLYGON_API_KEY
96
+
97
+ # 2. score
98
+ newsscore score AAPL # all saved sources, last 7 days
99
+ newsscore score AAPL -s finnhub -s yahoo -d 3 # only these sources, last 3 days
100
+ newsscore score AAPL --scorer keyword -a 10 # force the offline scorer, show 10 articles
101
+ newsscore score AAPL --json | jq .score
102
+
103
+ # 3. look around
104
+ newsscore fetch AAPL # articles only, no scoring
105
+ newsscore fetch AAPL --out news.json # ...saved as JSON instead of printed
106
+ newsscore score AAPL --out aapl.json # full result, every scored article
107
+ newsscore source list | types | remove NAME
108
+ newsscore doctor # shows every path in use
109
+ ```
110
+
111
+ Sample output (real run, four free-tier sources, Jev scorer):
112
+
113
+ ```
114
+ query AAPL
115
+ window 2026-09-10 .. 2026-09-17 (7 days)
116
+ scorer jev-v1
117
+ articles 310
118
+ score +0.120 (-1 bearish .. +1 bullish)
119
+ confidence 1.000
120
+ by source finnhub=+0.10 newsapi=+0.25 polygon=+0.28 yahoo=+0.10
121
+
122
+ +0.50 c=0.98 r=0.97 09-17 21:45 [yahoo] Apple (AAPL) Outperforms Broader Market: What You Need to Know
123
+ -0.01 c=0.99 r=0.02 09-17 20:20 [yahoo] Should You Buy HP Stock For The Shares It Keeps Retiring?
124
+ -0.98 c=0.97 r=0.97 09-17 16:44 [yahoo] Apple (AAPL) Faces a $2.7 Billion UK Lawsuit over its App Tracking Rules
125
+ ```
126
+
127
+ `c` is Jev's confidence and `r` its relevance; the HP article is correctly weighted
128
+ out. 310 articles took about 15 s to score the first time and under 2 s from cache.
129
+
130
+ ## Quick start: Python
131
+
132
+ ```python
133
+ from newsscore import NewsScorer
134
+
135
+ scorer = NewsScorer() # Jev if TYPESAFE_API_KEY is set, else keyword scorer
136
+ scorer.source_add("finnhub", api_key="...")
137
+ scorer.source_add("yahoo") # keyless
138
+ scorer.source_add("rss", name="ft", url="https://www.ft.com/companies?format=rss")
139
+
140
+ result = scorer.score("AAPL", days=7) # blocking
141
+ print(result.score, result.confidence, result.n_articles)
142
+ for item in result.articles[:5]:
143
+ print(item.score.score, item.article.title)
144
+ ```
145
+
146
+ Inside an async pipeline use the `a`-prefixed methods; everything network-bound is
147
+ `async` and sources are fetched concurrently:
148
+
149
+ ```python
150
+ import asyncio
151
+ from newsscore import NewsScorer
152
+
153
+ async def main():
154
+ scorer = NewsScorer.from_config() # loads the sources saved by the CLI
155
+ results = await asyncio.gather(*(scorer.ascore(s, days=3) for s in ["AAPL", "MSFT", "NVDA"]))
156
+ for r in results:
157
+ print(r.query, f"{r.score:+.3f}", f"conf={r.confidence:.2f}", r.n_articles)
158
+ await scorer.aclose()
159
+
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ Pass your own `httpx.AsyncClient` with `NewsScorer(http_client=client)` to share
164
+ connection pools with the rest of your pipeline. `result.to_dict()` gives plain JSON.
165
+
166
+ ## Bring your own scoring function
167
+
168
+ The scorer is just a callable. Pass it as `score_fn`:
169
+
170
+ ```python
171
+ from newsscore import NewsScorer, Article, ArticleScore, per_article
172
+
173
+ # batch form (preferred): one call per batch of up to `batch_size` articles
174
+ async def my_scorer(articles: list[Article], query: str) -> list[ArticleScore]:
175
+ texts = [a.text for a in articles]
176
+ probs = await my_model.predict(texts) # your code
177
+ return [ArticleScore(score=2 * p - 1, confidence=abs(2 * p - 1)) for p in probs]
178
+
179
+ my_scorer.name = "my-model-v3" # stable cache key
180
+ scorer = NewsScorer(score_fn=my_scorer, batch_size=32, concurrency=2)
181
+
182
+ # or one article at a time
183
+ scorer = NewsScorer(score_fn=per_article(lambda a, q: 0.5 if "beat" in a.text.lower() else 0.0),
184
+ scorer_name="beat-rule")
185
+ ```
186
+
187
+ ### Contract
188
+
189
+ | | |
190
+ |---|---|
191
+ | **Signature** | `fn(articles: Sequence[Article], query: str)`, plain or `async` |
192
+ | **Input** | `articles`: the batch (see `batch_size`, default 16). `query`: the symbol or keyword they were fetched for. Each `Article` has `id`, `source`, `title`, `published` (UTC), `url`, `summary`, `symbols`, `raw` (vendor payload) and `text` (title + summary). |
193
+ | **Output** | A sequence with **one item per input article, same order**. Each item is an `ArticleScore`, **or** a number in `[-1, 1]`, **or** a dict `{"score": float, "confidence"?: float, "relevance"?: float, "labels"?: dict}`. |
194
+ | `score` | `-1.0` very bearish .. `+1.0` very bullish. |
195
+ | `confidence` | `[0, 1]`, how sure the scorer is. Aggregation weight. Default `1.0`. |
196
+ | `relevance` | `[0, 1]`, how much the article is about `query`. Aggregation weight. Default `1.0`. |
197
+ | `labels` | Anything you want to keep (category, probabilities, model version). Stored in the cache. |
198
+ | **Errors** | An exception fails only that batch. The message lands in `ScoreResult.errors`; other batches proceed. |
199
+ | **Caching** | Results are cached under `(scorer name, query, article id)`. Set `fn.name` or `NewsScorer(scorer_name=...)`; anonymous lambdas are not cached. |
200
+
201
+ The full contract also lives in the docstring of `newsscore/scoring/protocol.py`.
202
+
203
+ ## Built-in scorers
204
+
205
+ | name | needs | what it does |
206
+ |---|---|---|
207
+ | `jev` (default when available) | `pip install ".[jev]"`, `TYPESAFE_API_KEY` | One Jev `system_one` call per article asking a 5-level sentiment `Score`, a relevance `Noul`, an event `Choice` (earnings, guidance, M&A, legal, product, analyst, management, macro, other) and a novelty `Noul`. Score is the probability-weighted level rescaled to `[-1, 1]`; confidence is Jev's calibrated confidence. |
208
+ | `keyword` | nothing | Small Loughran-McDonald-style lexicon with negation handling. Offline fallback and test double, not a trading signal. |
209
+
210
+ Select explicitly with `NewsScorer(score_fn="keyword")` or `newsscore score --scorer keyword`.
211
+ Tune Jev with `NewsScorer(score_fn=JevScorer(concurrency=16, model="jev-latest"))`.
212
+
213
+ ## News sources
214
+
215
+ | type | key env var | query | notes |
216
+ |---|---|---|---|
217
+ | `finnhub` | `FINNHUB_API_KEY` | ticker | company-news endpoint |
218
+ | `alpha_vantage` | `ALPHAVANTAGE_API_KEY` | ticker | vendor sentiment kept in `Article.raw` |
219
+ | `polygon` / `massive` | `POLYGON_API_KEY` or `MASSIVE_API_KEY` | ticker | Polygon.io is now Massive.com; same keys. Follows `next_url`; option `max_pages` (5) |
220
+ | `tiingo` | `TIINGO_API_KEY` | ticker | option `limit` (1000) |
221
+ | `marketaux` | `MARKETAUX_API_KEY` | ticker | option `max_pages` (3), `language` |
222
+ | `newsapi` | `NEWSAPI_API_KEY` | keyword | pass a company name; option `max_pages` (1), `language` |
223
+ | `rss` | none | keyword | option `url` (use `{query}` as placeholder), `match` (true) |
224
+ | `yahoo` | none | ticker | Yahoo Finance headline RSS preset |
225
+
226
+ Add a source by type and options:
227
+
228
+ ```bash
229
+ newsscore source add marketaux --api-key KEY -o max_pages=5
230
+ newsscore source add rss --name sec -o url="https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent&output=atom"
231
+ ```
232
+
233
+ or in code: `scorer.source_add("marketaux", api_key="KEY", max_pages=5)`.
234
+
235
+ ### Writing a source
236
+
237
+ ```python
238
+ from newsscore import NewsSource, register
239
+
240
+ @register
241
+ class MySource(NewsSource):
242
+ type_name = "mine"
243
+ env_key = "MINE_API_KEY" # or requires_key = False
244
+
245
+ async def fetch(self, query, since, until, client):
246
+ data = await self._get_json(client, "https://...", params={"q": query, "key": self.api_key})
247
+ return [self._article(title=d["title"], published=parse_dt(d["ts"]), url=d["url"]) for d in data]
248
+ ```
249
+
250
+ `since`/`until` are aware UTC datetimes; `client` is a shared `httpx.AsyncClient`.
251
+ Registered types are available to the CLI too.
252
+
253
+ ## How the aggregate is computed
254
+
255
+ ```
256
+ weight_i = confidence_i * relevance_i * 0.5 ** (age_hours_i / half_life_hours) # half_life 48h
257
+ score = sum(weight_i * score_i) / sum(weight_i)
258
+ confidence = 1 - exp(-sum(weight_i) / 3) # ~0.63 with three solid fresh articles
259
+ ```
260
+
261
+ Change the half-life with `NewsScorer(half_life_hours=24)` or replace the whole thing
262
+ with `NewsScorer(aggregate_fn=my_fn)` where `my_fn(scored, now) -> Aggregate`.
263
+
264
+ ## Where things are stored
265
+
266
+ `newsscore` writes nothing you did not ask for. Run `newsscore doctor` to print the
267
+ actual paths on your machine.
268
+
269
+ | what | where | how to change it |
270
+ |---|---|---|
271
+ | **fetched articles** | nowhere — printed to the terminal | `--out FILE` to save them as JSON, or `--json` and redirect |
272
+ | **scored results** | nowhere — printed to the terminal | `--out FILE`, or `--json` and redirect |
273
+ | **article scores** (the cache) | `platformdirs.user_cache_dir("newsscore")/scores.sqlite` | `NEWSSCORE_CACHE`; `--no-cache` / `cache=False` to skip it; `newsscore cache-clear` to wipe it |
274
+ | **saved sources** | `platformdirs.user_config_dir("newsscore")/sources.json` | `NEWSSCORE_CONFIG`; `newsscore config-path` to print it |
275
+ | **API keys** | `.env` in the working directory | `NEWSSCORE_ENV`, or real environment variables |
276
+
277
+ ### Saving articles and results
278
+
279
+ ```bash
280
+ newsscore fetch AAPL -d 7 --out aapl-articles.json # every article, one JSON array
281
+ newsscore score AAPL --out runs/aapl-2026-09-18.json # aggregate + every scored article, dirs created
282
+ ```
283
+
284
+ `--out` creates missing parent directories and always writes UTF-8, which the shell's
285
+ own `>` redirection does not reliably do on Windows. In Python, `result.to_dict()` and
286
+ `article.to_dict()` give the same structures, so you can send them wherever you like:
287
+
288
+ ```python
289
+ import json
290
+ result = scorer.score("AAPL", days=7)
291
+ Path("aapl.json").write_text(json.dumps(result.to_dict(), indent=2, default=str), encoding="utf-8")
292
+ ```
293
+
294
+ ### The score cache
295
+
296
+ Scores — not articles — are cached under `(scorer name, query, article id)`, so re-running
297
+ a query only pays for articles you have not seen before, and back-tests replay for free.
298
+ It is an ordinary SQLite file; point `NEWSSCORE_CACHE` at a project directory if you want
299
+ one cache per research project rather than one per user.
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ uv sync # creates .venv with the package, the Jev SDK and test tools
305
+ uv run newsscore doctor
306
+ uv run pytest
307
+ ```
308
+
309
+ Or with plain pip: `pip install -e ".[jev,dev]"` then `pytest`.
310
+
311
+ Test artefacts (per-test config files and score caches) are written under
312
+ `E:\test_data\jev_sentiment` when that drive exists; set `NEWSSCORE_TEST_DATA` to move them,
313
+ or they fall back to pytest's temporary directory.
314
+
315
+ The design notes are in [PLAN.md](PLAN.md).
316
+
317
+ ## Status: what has and has not been tested live
318
+
319
+ This is a 0.1 release built and verified on one machine with the API plans its
320
+ author happens to have. Every source has unit tests against fixture payloads that
321
+ follow the provider's documented response shape, but only some have been run
322
+ against the real endpoint.
323
+
324
+ | Component | Unit tests | Live-tested | Notes |
325
+ |---|---|---|---|
326
+ | `finnhub` | yes | **yes** | free tier, 237 AAPL articles / 7 days |
327
+ | `polygon` / `massive` | yes | **yes** | free tier via `api.massive.com`, pagination exercised |
328
+ | `newsapi` | yes | **yes** | developer plan (24 h delay) |
329
+ | `yahoo` RSS | yes | **yes** | keyless |
330
+ | `jev` scorer | yes (fake client) | **yes** | 310 articles scored with a real `TYPESAFE_API_KEY` |
331
+ | `alpha_vantage` | yes | **no** | no key available; quota-message handling untested live |
332
+ | `marketaux` | yes | **no** | no key available; page-size behaviour on the free plan unverified |
333
+ | `tiingo` | yes | **no** | live call returned 403 on the free plan, so the parser has never seen real data |
334
+ | generic `rss` (Atom) | yes | **no** | Atom branch only covered by a fixture; RSS 2.0 covered via Yahoo |
335
+ | `keyword` scorer | yes | n/a | offline |
336
+
337
+ Untested does not mean broken, but field names and pagination details are exactly
338
+ where providers drift from their docs. If you hold a key for one of the untested
339
+ sources, running `newsscore fetch AAPL -s <source>` and reporting the outcome is the
340
+ single most useful contribution right now.
341
+
342
+ ## Contributing
343
+
344
+ Testers, bug reports and pull requests are all welcome.
345
+
346
+ * **Testers.** Run `newsscore doctor`, then `newsscore fetch` and `newsscore score` against
347
+ any source you have a key for. Open an issue with the provider, plan tier, the
348
+ command, and the output (redact your key). A short "works for me" note is useful too.
349
+ * **New sources.** Subclass `NewsSource`, implement `fetch`, register the class, add
350
+ a fixture test in `tests/test_sources.py` and a row to the sources table above.
351
+ See "Writing a source" for the shape.
352
+ * **New scorers.** Anything matching the `ScoreFn` contract can be added to
353
+ `newsscore/scoring/` and registered in `SCORERS`.
354
+ * **Pull requests.** Keep them focused, run `uv run pytest` before pushing, and
355
+ update the README table when you change what is tested. Don't commit `.env`.
356
+
357
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the short version of the workflow.
358
+
359
+ ## Caveats
360
+
361
+ Jev launched in September 2026 and its accuracy on financial text has not been
362
+ independently benchmarked. Build a small hand-labelled set and compare `jev`
363
+ against your alternatives before trusting any signal. Headline sentiment on large
364
+ caps is largely priced in within minutes; treat the aggregate as a research input,
365
+ not a trade trigger.