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.
- newsscore-0.1.0/.github/workflows/ci.yml +24 -0
- newsscore-0.1.0/.github/workflows/publish.yml +46 -0
- newsscore-0.1.0/.gitignore +8 -0
- newsscore-0.1.0/CONTRIBUTING.md +50 -0
- newsscore-0.1.0/LICENSE +21 -0
- newsscore-0.1.0/PKG-INFO +365 -0
- newsscore-0.1.0/README.md +338 -0
- newsscore-0.1.0/pyproject.toml +55 -0
- newsscore-0.1.0/src/newsscore/__init__.py +42 -0
- newsscore-0.1.0/src/newsscore/_version.py +1 -0
- newsscore-0.1.0/src/newsscore/aggregate.py +78 -0
- newsscore-0.1.0/src/newsscore/cache.py +99 -0
- newsscore-0.1.0/src/newsscore/cli.py +312 -0
- newsscore-0.1.0/src/newsscore/config.py +151 -0
- newsscore-0.1.0/src/newsscore/http.py +21 -0
- newsscore-0.1.0/src/newsscore/models.py +163 -0
- newsscore-0.1.0/src/newsscore/scorer.py +299 -0
- newsscore-0.1.0/src/newsscore/scoring/__init__.py +56 -0
- newsscore-0.1.0/src/newsscore/scoring/jev.py +173 -0
- newsscore-0.1.0/src/newsscore/scoring/keyword.py +94 -0
- newsscore-0.1.0/src/newsscore/scoring/protocol.py +119 -0
- newsscore-0.1.0/src/newsscore/sources/__init__.py +68 -0
- newsscore-0.1.0/src/newsscore/sources/alpha_vantage.py +55 -0
- newsscore-0.1.0/src/newsscore/sources/base.py +154 -0
- newsscore-0.1.0/src/newsscore/sources/finnhub.py +43 -0
- newsscore-0.1.0/src/newsscore/sources/marketaux.py +54 -0
- newsscore-0.1.0/src/newsscore/sources/newsapi.py +59 -0
- newsscore-0.1.0/src/newsscore/sources/polygon.py +71 -0
- newsscore-0.1.0/src/newsscore/sources/rss.py +103 -0
- newsscore-0.1.0/src/newsscore/sources/tiingo.py +45 -0
- newsscore-0.1.0/tests/conftest.py +51 -0
- newsscore-0.1.0/tests/test_config.py +57 -0
- newsscore-0.1.0/tests/test_core.py +90 -0
- newsscore-0.1.0/tests/test_jev.py +69 -0
- newsscore-0.1.0/tests/test_scorer.py +139 -0
- 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,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`.
|
newsscore-0.1.0/LICENSE
ADDED
|
@@ -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.
|
newsscore-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|