alphai-haystack 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.
- alphai_haystack-0.1.0/.gitignore +29 -0
- alphai_haystack-0.1.0/CHANGELOG.md +12 -0
- alphai_haystack-0.1.0/LICENSE +21 -0
- alphai_haystack-0.1.0/PKG-INFO +170 -0
- alphai_haystack-0.1.0/README.md +137 -0
- alphai_haystack-0.1.0/pyproject.toml +87 -0
- alphai_haystack-0.1.0/src/alphai_haystack/__init__.py +12 -0
- alphai_haystack-0.1.0/src/alphai_haystack/_version.py +1 -0
- alphai_haystack-0.1.0/src/alphai_haystack/fetchers.py +252 -0
- alphai_haystack-0.1.0/src/alphai_haystack/py.typed +0 -0
- alphai_haystack-0.1.0/tests/conftest.py +88 -0
- alphai_haystack-0.1.0/tests/test_fetchers.py +110 -0
- alphai_haystack-0.1.0/tests/test_serialization.py +62 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.python-version.local
|
|
11
|
+
|
|
12
|
+
# Tooling caches
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# Env / secrets
|
|
20
|
+
.env
|
|
21
|
+
*.local
|
|
22
|
+
|
|
23
|
+
# OS / editor
|
|
24
|
+
.DS_Store
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
|
|
28
|
+
# uv
|
|
29
|
+
uv.lock
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-07-28
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `AlphaAINewsFetcher`: the scored news feed as Haystack Documents, with `symbol` /
|
|
8
|
+
`category` / `min_relevance` / `collapse_stories` / `top_k` filters and per-run overrides.
|
|
9
|
+
- `AlphaAIInsiderNewsFetcher`: SEC Form 4 insider events with a structured
|
|
10
|
+
`meta["insider"]` block.
|
|
11
|
+
- Pipeline-safe serialization (`to_dict` / `from_dict`, API key as a `Secret`
|
|
12
|
+
env-var reference).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AlphaAI
|
|
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,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alphai-haystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Haystack components for AlphaAI: AI-scored financial news and SEC Form 4 insider events as Documents.
|
|
5
|
+
Project-URL: Homepage, https://alphai.io
|
|
6
|
+
Project-URL: Documentation, https://alphai.io/developers
|
|
7
|
+
Project-URL: Repository, https://github.com/makeev/alphai-haystack
|
|
8
|
+
Project-URL: API Reference, https://api.alphai.io/api/schema/
|
|
9
|
+
Author-email: AlphaAI <support@alphai.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,alphai,financial-news,haystack,insider,llm,rag,sec,stocks
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: alphai-sdk<1,>=0.4.1
|
|
27
|
+
Requires-Dist: haystack-ai<3,>=2.6
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff<0.17,>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# alphai-haystack
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/alphai-haystack/)
|
|
37
|
+
[](https://github.com/makeev/alphai-haystack/actions/workflows/ci.yml)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
[Haystack](https://haystack.deepset.ai) components for [AlphaAI](https://alphai.io) — AI-scored
|
|
41
|
+
financial news and SEC Form 4 insider events, delivered as Haystack `Document` objects.
|
|
42
|
+
|
|
43
|
+
Every article on the AlphaAI feed is enriched at ingest: per-ticker impact analysis, a category,
|
|
44
|
+
and a 1-10 relevance score. SEC Form 4 filings become structured insider events about 6 minutes
|
|
45
|
+
after they hit EDGAR. These components fetch that feed so your pipelines and agents can reason
|
|
46
|
+
over pre-scored market news instead of raw headlines.
|
|
47
|
+
|
|
48
|
+
## Components
|
|
49
|
+
|
|
50
|
+
- **`AlphaAINewsFetcher`** — the main news feed. Filter by ticker, category, and a relevance
|
|
51
|
+
floor; optionally collapse same-story coverage into one item.
|
|
52
|
+
- **`AlphaAIInsiderNewsFetcher`** — SEC Form 4 insider events with a structured
|
|
53
|
+
`meta["insider"]` block: side, shares, average price, total value, who traded, and whether it
|
|
54
|
+
was a pre-planned 10b5-1 sale.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install alphai-haystack
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API key
|
|
63
|
+
|
|
64
|
+
Get a free key at [alphai.io/developers](https://alphai.io/developers) (free tier: 20 requests
|
|
65
|
+
per minute, 100 per day, no card). The components read it from the `ALPHAI_API_KEY` environment
|
|
66
|
+
variable by default:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export ALPHAI_API_KEY="ak_..."
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Standalone
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from alphai_haystack import AlphaAINewsFetcher
|
|
78
|
+
|
|
79
|
+
fetcher = AlphaAINewsFetcher(symbol="NVDA", min_relevance=7)
|
|
80
|
+
documents = fetcher.run()["documents"]
|
|
81
|
+
|
|
82
|
+
for doc in documents:
|
|
83
|
+
print(doc.meta["relevance_score"], doc.meta["title"])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Insider events
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from alphai_haystack import AlphaAIInsiderNewsFetcher
|
|
90
|
+
|
|
91
|
+
fetcher = AlphaAIInsiderNewsFetcher(min_relevance=7) # higher floor = larger trades
|
|
92
|
+
for doc in fetcher.run()["documents"]:
|
|
93
|
+
insider = doc.meta["insider"]
|
|
94
|
+
print(insider["insider_name"], insider["side"], insider["total_value_usd"], doc.meta["tickers"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### In a pipeline
|
|
98
|
+
|
|
99
|
+
A minimal market-brief pipeline: fetch scored news for a ticker, hand it to an LLM.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from haystack import Pipeline
|
|
103
|
+
from haystack.components.builders import PromptBuilder
|
|
104
|
+
from haystack.components.generators import OpenAIGenerator
|
|
105
|
+
|
|
106
|
+
from alphai_haystack import AlphaAINewsFetcher
|
|
107
|
+
|
|
108
|
+
template = """Summarize what moved {{ symbol }} today, using only these articles:
|
|
109
|
+
{% for doc in documents %}
|
|
110
|
+
- {{ doc.content }} (relevance {{ doc.meta.relevance_score }}/10)
|
|
111
|
+
{% endfor %}
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
pipeline = Pipeline()
|
|
115
|
+
pipeline.add_component("news", AlphaAINewsFetcher(min_relevance=6, collapse_stories=True))
|
|
116
|
+
pipeline.add_component("prompt", PromptBuilder(template=template))
|
|
117
|
+
pipeline.add_component("llm", OpenAIGenerator(model="gpt-4o-mini"))
|
|
118
|
+
pipeline.connect("news.documents", "prompt.documents")
|
|
119
|
+
pipeline.connect("prompt", "llm")
|
|
120
|
+
|
|
121
|
+
result = pipeline.run({"news": {"symbol": "NVDA"}, "prompt": {"symbol": "NVDA"}})
|
|
122
|
+
print(result["llm"]["replies"][0])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Both components implement `to_dict`/`from_dict`, so pipelines serialize to YAML and back; the
|
|
126
|
+
API key is stored as an environment-variable reference, never as the raw value.
|
|
127
|
+
|
|
128
|
+
## Document shape
|
|
129
|
+
|
|
130
|
+
`content` is the article title plus summary. `meta` carries:
|
|
131
|
+
|
|
132
|
+
| Key | Type | Notes |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `uid` | str | Stable article id (use with the AlphaAI article endpoint) |
|
|
135
|
+
| `url` | str | Original article URL |
|
|
136
|
+
| `title`, `source`, `source_domain` | str | |
|
|
137
|
+
| `published_at` | str | ISO 8601 |
|
|
138
|
+
| `tickers` | list[str] | Tickers the article affects |
|
|
139
|
+
| `category` | str | One of 14 categories (`earnings`, `insider`, `crypto`, ...) |
|
|
140
|
+
| `relevance_score` | int | 1-10, assigned at ingest |
|
|
141
|
+
| `sources_count` | int | Only when `collapse_stories=True` |
|
|
142
|
+
| `insider` | dict | Insider feed only: side, shares, avg price, total value, who |
|
|
143
|
+
|
|
144
|
+
## Run parameters
|
|
145
|
+
|
|
146
|
+
`run()` accepts per-call overrides for the filters set in `__init__`: `symbol`, `category`
|
|
147
|
+
(news fetcher only), `min_relevance`, and `top_k`.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
ruff check . && ruff format --check .
|
|
154
|
+
mypy src/alphai_haystack
|
|
155
|
+
pytest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Tests run fully offline against a fake client.
|
|
159
|
+
|
|
160
|
+
## Links
|
|
161
|
+
|
|
162
|
+
- [AlphaAI developer docs](https://alphai.io/developers)
|
|
163
|
+
- [OpenAPI schema](https://api.alphai.io/api/schema/)
|
|
164
|
+
- [Python SDK (`alphai-sdk`)](https://github.com/makeev/alphai-sdk) — this package is a thin
|
|
165
|
+
Haystack layer over it
|
|
166
|
+
- [MCP server](https://alphai.io/mcp) — the same feed for MCP-speaking agents
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# alphai-haystack
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/alphai-haystack/)
|
|
4
|
+
[](https://github.com/makeev/alphai-haystack/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
[Haystack](https://haystack.deepset.ai) components for [AlphaAI](https://alphai.io) — AI-scored
|
|
8
|
+
financial news and SEC Form 4 insider events, delivered as Haystack `Document` objects.
|
|
9
|
+
|
|
10
|
+
Every article on the AlphaAI feed is enriched at ingest: per-ticker impact analysis, a category,
|
|
11
|
+
and a 1-10 relevance score. SEC Form 4 filings become structured insider events about 6 minutes
|
|
12
|
+
after they hit EDGAR. These components fetch that feed so your pipelines and agents can reason
|
|
13
|
+
over pre-scored market news instead of raw headlines.
|
|
14
|
+
|
|
15
|
+
## Components
|
|
16
|
+
|
|
17
|
+
- **`AlphaAINewsFetcher`** — the main news feed. Filter by ticker, category, and a relevance
|
|
18
|
+
floor; optionally collapse same-story coverage into one item.
|
|
19
|
+
- **`AlphaAIInsiderNewsFetcher`** — SEC Form 4 insider events with a structured
|
|
20
|
+
`meta["insider"]` block: side, shares, average price, total value, who traded, and whether it
|
|
21
|
+
was a pre-planned 10b5-1 sale.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install alphai-haystack
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API key
|
|
30
|
+
|
|
31
|
+
Get a free key at [alphai.io/developers](https://alphai.io/developers) (free tier: 20 requests
|
|
32
|
+
per minute, 100 per day, no card). The components read it from the `ALPHAI_API_KEY` environment
|
|
33
|
+
variable by default:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export ALPHAI_API_KEY="ak_..."
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Standalone
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from alphai_haystack import AlphaAINewsFetcher
|
|
45
|
+
|
|
46
|
+
fetcher = AlphaAINewsFetcher(symbol="NVDA", min_relevance=7)
|
|
47
|
+
documents = fetcher.run()["documents"]
|
|
48
|
+
|
|
49
|
+
for doc in documents:
|
|
50
|
+
print(doc.meta["relevance_score"], doc.meta["title"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Insider events
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from alphai_haystack import AlphaAIInsiderNewsFetcher
|
|
57
|
+
|
|
58
|
+
fetcher = AlphaAIInsiderNewsFetcher(min_relevance=7) # higher floor = larger trades
|
|
59
|
+
for doc in fetcher.run()["documents"]:
|
|
60
|
+
insider = doc.meta["insider"]
|
|
61
|
+
print(insider["insider_name"], insider["side"], insider["total_value_usd"], doc.meta["tickers"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### In a pipeline
|
|
65
|
+
|
|
66
|
+
A minimal market-brief pipeline: fetch scored news for a ticker, hand it to an LLM.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from haystack import Pipeline
|
|
70
|
+
from haystack.components.builders import PromptBuilder
|
|
71
|
+
from haystack.components.generators import OpenAIGenerator
|
|
72
|
+
|
|
73
|
+
from alphai_haystack import AlphaAINewsFetcher
|
|
74
|
+
|
|
75
|
+
template = """Summarize what moved {{ symbol }} today, using only these articles:
|
|
76
|
+
{% for doc in documents %}
|
|
77
|
+
- {{ doc.content }} (relevance {{ doc.meta.relevance_score }}/10)
|
|
78
|
+
{% endfor %}
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
pipeline = Pipeline()
|
|
82
|
+
pipeline.add_component("news", AlphaAINewsFetcher(min_relevance=6, collapse_stories=True))
|
|
83
|
+
pipeline.add_component("prompt", PromptBuilder(template=template))
|
|
84
|
+
pipeline.add_component("llm", OpenAIGenerator(model="gpt-4o-mini"))
|
|
85
|
+
pipeline.connect("news.documents", "prompt.documents")
|
|
86
|
+
pipeline.connect("prompt", "llm")
|
|
87
|
+
|
|
88
|
+
result = pipeline.run({"news": {"symbol": "NVDA"}, "prompt": {"symbol": "NVDA"}})
|
|
89
|
+
print(result["llm"]["replies"][0])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Both components implement `to_dict`/`from_dict`, so pipelines serialize to YAML and back; the
|
|
93
|
+
API key is stored as an environment-variable reference, never as the raw value.
|
|
94
|
+
|
|
95
|
+
## Document shape
|
|
96
|
+
|
|
97
|
+
`content` is the article title plus summary. `meta` carries:
|
|
98
|
+
|
|
99
|
+
| Key | Type | Notes |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `uid` | str | Stable article id (use with the AlphaAI article endpoint) |
|
|
102
|
+
| `url` | str | Original article URL |
|
|
103
|
+
| `title`, `source`, `source_domain` | str | |
|
|
104
|
+
| `published_at` | str | ISO 8601 |
|
|
105
|
+
| `tickers` | list[str] | Tickers the article affects |
|
|
106
|
+
| `category` | str | One of 14 categories (`earnings`, `insider`, `crypto`, ...) |
|
|
107
|
+
| `relevance_score` | int | 1-10, assigned at ingest |
|
|
108
|
+
| `sources_count` | int | Only when `collapse_stories=True` |
|
|
109
|
+
| `insider` | dict | Insider feed only: side, shares, avg price, total value, who |
|
|
110
|
+
|
|
111
|
+
## Run parameters
|
|
112
|
+
|
|
113
|
+
`run()` accepts per-call overrides for the filters set in `__init__`: `symbol`, `category`
|
|
114
|
+
(news fetcher only), `min_relevance`, and `top_k`.
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install -e ".[dev]"
|
|
120
|
+
ruff check . && ruff format --check .
|
|
121
|
+
mypy src/alphai_haystack
|
|
122
|
+
pytest
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Tests run fully offline against a fake client.
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
- [AlphaAI developer docs](https://alphai.io/developers)
|
|
130
|
+
- [OpenAPI schema](https://api.alphai.io/api/schema/)
|
|
131
|
+
- [Python SDK (`alphai-sdk`)](https://github.com/makeev/alphai-sdk) — this package is a thin
|
|
132
|
+
Haystack layer over it
|
|
133
|
+
- [MCP server](https://alphai.io/mcp) — the same feed for MCP-speaking agents
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "alphai-haystack"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Haystack components for AlphaAI: AI-scored financial news and SEC Form 4 insider events as Documents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "AlphaAI", email = "support@alphai.io" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"haystack",
|
|
16
|
+
"alphai",
|
|
17
|
+
"financial-news",
|
|
18
|
+
"stocks",
|
|
19
|
+
"sec",
|
|
20
|
+
"insider",
|
|
21
|
+
"rag",
|
|
22
|
+
"llm",
|
|
23
|
+
"agents",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = [
|
|
40
|
+
"haystack-ai>=2.6,<3",
|
|
41
|
+
"alphai-sdk>=0.4.1,<1",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://alphai.io"
|
|
46
|
+
Documentation = "https://alphai.io/developers"
|
|
47
|
+
Repository = "https://github.com/makeev/alphai-haystack"
|
|
48
|
+
"API Reference" = "https://api.alphai.io/api/schema/"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=8",
|
|
53
|
+
# Capped on purpose: CI runs `ruff format --check .`, and a ruff minor can
|
|
54
|
+
# widen what "formatted" means (0.16 started formatting Markdown code blocks).
|
|
55
|
+
# Bump deliberately, reformat in the same commit.
|
|
56
|
+
"ruff>=0.6,<0.17",
|
|
57
|
+
"mypy>=1.10",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.version]
|
|
61
|
+
path = "src/alphai_haystack/_version.py"
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/alphai_haystack"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = ["/src", "/tests", "/README.md", "/LICENSE", "/CHANGELOG.md"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
line-length = 100
|
|
71
|
+
target-version = "py310"
|
|
72
|
+
src = ["src", "tests"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "C4", "PIE", "RUF"]
|
|
76
|
+
ignore = ["B008"]
|
|
77
|
+
|
|
78
|
+
# python_version is 3.12 (not our 3.10 floor) because numpy 2.x stubs use
|
|
79
|
+
# PEP 695 `type` statements, which mypy refuses to parse under 3.10/3.11.
|
|
80
|
+
# 3.10 compatibility is held by ruff (target-version py310) and the CI matrix.
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.12"
|
|
83
|
+
strict = true
|
|
84
|
+
files = ["src/alphai_haystack"]
|
|
85
|
+
|
|
86
|
+
[tool.pytest.ini_options]
|
|
87
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Haystack components for the AlphaAI financial-news API (alphai.io)."""
|
|
2
|
+
|
|
3
|
+
from ._version import VERSION
|
|
4
|
+
from .fetchers import AlphaAIInsiderNewsFetcher, AlphaAINewsFetcher
|
|
5
|
+
|
|
6
|
+
__version__ = VERSION
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AlphaAIInsiderNewsFetcher",
|
|
10
|
+
"AlphaAINewsFetcher",
|
|
11
|
+
"__version__",
|
|
12
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.1.0"
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Haystack components that turn AlphaAI's scored news feed into Documents.
|
|
2
|
+
|
|
3
|
+
Every article on the feed already carries an AI enrichment layer (per-ticker
|
|
4
|
+
impact analysis, a category, a 1-10 relevance score), so the components here
|
|
5
|
+
do no scoring of their own — they fetch, filter, and map articles into
|
|
6
|
+
:class:`haystack.Document` objects with the enrichment exposed as metadata.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import date, datetime
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from alphai import Client
|
|
16
|
+
from alphai.models import RichNewsArticle
|
|
17
|
+
from haystack import Document, component, default_from_dict, default_to_dict
|
|
18
|
+
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
19
|
+
|
|
20
|
+
DEFAULT_TOP_K = 10
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _plain(value: object) -> object:
|
|
24
|
+
"""Convert SDK field values into JSON-friendly meta values."""
|
|
25
|
+
if isinstance(value, Decimal):
|
|
26
|
+
return str(value)
|
|
27
|
+
if isinstance(value, (datetime, date)):
|
|
28
|
+
return value.isoformat()
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _category_str(category: object) -> str:
|
|
33
|
+
return str(getattr(category, "value", category))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _to_document(article: RichNewsArticle) -> Document:
|
|
37
|
+
original = article.original
|
|
38
|
+
enrichment = article.enrichment
|
|
39
|
+
parts = [original.title.strip(), original.summary.strip()]
|
|
40
|
+
content = "\n\n".join(part for part in parts if part)
|
|
41
|
+
meta: dict[str, Any] = {
|
|
42
|
+
"uid": original.uid,
|
|
43
|
+
"url": original.url,
|
|
44
|
+
"title": original.title,
|
|
45
|
+
"source": original.source,
|
|
46
|
+
"source_domain": original.source_domain,
|
|
47
|
+
"published_at": _plain(original.time_published),
|
|
48
|
+
"tickers": list(enrichment.tickers),
|
|
49
|
+
"category": _category_str(enrichment.category),
|
|
50
|
+
"relevance_score": enrichment.relevance_score,
|
|
51
|
+
}
|
|
52
|
+
if article.sources_count is not None:
|
|
53
|
+
meta["sources_count"] = article.sources_count
|
|
54
|
+
if article.insider is not None:
|
|
55
|
+
event = article.insider
|
|
56
|
+
meta["insider"] = {
|
|
57
|
+
"side": event.side,
|
|
58
|
+
"transaction_code": event.transaction_code,
|
|
59
|
+
"shares": _plain(event.shares),
|
|
60
|
+
"avg_price_usd": _plain(event.avg_price_usd),
|
|
61
|
+
"total_value_usd": _plain(event.total_value_usd),
|
|
62
|
+
"is_10b5_1": event.is_10b5_1,
|
|
63
|
+
"insider_name": event.insider_name,
|
|
64
|
+
"insider_title": event.insider_title,
|
|
65
|
+
"is_officer": event.is_officer,
|
|
66
|
+
"is_director": event.is_director,
|
|
67
|
+
"is_ten_percent_owner": event.is_ten_percent_owner,
|
|
68
|
+
"transaction_date": _plain(event.transaction_date),
|
|
69
|
+
}
|
|
70
|
+
return Document(content=content, meta=meta)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@component
|
|
74
|
+
class AlphaAINewsFetcher:
|
|
75
|
+
"""Fetches AI-scored financial news from AlphaAI as Haystack Documents.
|
|
76
|
+
|
|
77
|
+
Each Document's ``content`` is the article title plus summary; ``meta``
|
|
78
|
+
carries the enrichment (tickers, category, 1-10 ``relevance_score``,
|
|
79
|
+
source, url, publish time). Filters set in ``__init__`` are defaults and
|
|
80
|
+
can be overridden per ``run()`` call.
|
|
81
|
+
|
|
82
|
+
Requires an AlphaAI API key (free tier available, no card) — see
|
|
83
|
+
https://alphai.io/developers. The key is read from the ``ALPHAI_API_KEY``
|
|
84
|
+
environment variable by default.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from alphai_haystack import AlphaAINewsFetcher
|
|
88
|
+
|
|
89
|
+
fetcher = AlphaAINewsFetcher(symbol="NVDA", min_relevance=7)
|
|
90
|
+
documents = fetcher.run()["documents"]
|
|
91
|
+
```
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
api_key: Secret = Secret.from_env_var("ALPHAI_API_KEY"),
|
|
97
|
+
symbol: str | None = None,
|
|
98
|
+
category: str | None = None,
|
|
99
|
+
min_relevance: int | None = None,
|
|
100
|
+
collapse_stories: bool = False,
|
|
101
|
+
top_k: int = DEFAULT_TOP_K,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""
|
|
104
|
+
:param api_key: AlphaAI API key. Defaults to the ``ALPHAI_API_KEY`` env var.
|
|
105
|
+
:param symbol: Only articles tagged with this ticker (e.g. ``"NVDA"``).
|
|
106
|
+
:param category: Only articles in this category (e.g. ``"earnings"``,
|
|
107
|
+
``"mergers_acquisitions"``, ``"insider"``).
|
|
108
|
+
:param min_relevance: Only articles scored at or above this 1-10 floor.
|
|
109
|
+
:param collapse_stories: Collapse same-story coverage into one item.
|
|
110
|
+
:param top_k: Maximum number of Documents to return per run.
|
|
111
|
+
"""
|
|
112
|
+
if top_k < 1:
|
|
113
|
+
raise ValueError("top_k must be at least 1")
|
|
114
|
+
self.api_key = api_key
|
|
115
|
+
self.symbol = symbol
|
|
116
|
+
self.category = category
|
|
117
|
+
self.min_relevance = min_relevance
|
|
118
|
+
self.collapse_stories = collapse_stories
|
|
119
|
+
self.top_k = top_k
|
|
120
|
+
self._client: Client | None = None
|
|
121
|
+
|
|
122
|
+
def warm_up(self) -> None:
|
|
123
|
+
"""Create the underlying API client once."""
|
|
124
|
+
if self._client is None:
|
|
125
|
+
self._client = Client(api_key=self.api_key.resolve_value())
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> dict[str, Any]:
|
|
128
|
+
return default_to_dict(
|
|
129
|
+
self,
|
|
130
|
+
api_key=self.api_key.to_dict(),
|
|
131
|
+
symbol=self.symbol,
|
|
132
|
+
category=self.category,
|
|
133
|
+
min_relevance=self.min_relevance,
|
|
134
|
+
collapse_stories=self.collapse_stories,
|
|
135
|
+
top_k=self.top_k,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_dict(cls, data: dict[str, Any]) -> AlphaAINewsFetcher:
|
|
140
|
+
deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
|
|
141
|
+
result: AlphaAINewsFetcher = default_from_dict(cls, data)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
@component.output_types(documents=list[Document])
|
|
145
|
+
def run(
|
|
146
|
+
self,
|
|
147
|
+
symbol: str | None = None,
|
|
148
|
+
category: str | None = None,
|
|
149
|
+
min_relevance: int | None = None,
|
|
150
|
+
top_k: int | None = None,
|
|
151
|
+
) -> dict[str, list[Document]]:
|
|
152
|
+
"""Fetch the newest matching articles.
|
|
153
|
+
|
|
154
|
+
:param symbol: Overrides the ticker filter set in ``__init__``.
|
|
155
|
+
:param category: Overrides the category filter set in ``__init__``.
|
|
156
|
+
:param min_relevance: Overrides the relevance floor set in ``__init__``.
|
|
157
|
+
:param top_k: Overrides the maximum number of Documents.
|
|
158
|
+
:returns: ``{"documents": [...]}`` — newest first.
|
|
159
|
+
"""
|
|
160
|
+
self.warm_up()
|
|
161
|
+
assert self._client is not None
|
|
162
|
+
articles = self._client.news.iter(
|
|
163
|
+
symbol=symbol if symbol is not None else self.symbol,
|
|
164
|
+
category=category if category is not None else self.category,
|
|
165
|
+
min_relevance=min_relevance if min_relevance is not None else self.min_relevance,
|
|
166
|
+
collapse_stories=self.collapse_stories,
|
|
167
|
+
max_items=top_k if top_k is not None else self.top_k,
|
|
168
|
+
)
|
|
169
|
+
return {"documents": [_to_document(article) for article in articles]}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@component
|
|
173
|
+
class AlphaAIInsiderNewsFetcher:
|
|
174
|
+
"""Fetches SEC Form 4 insider-trading events from AlphaAI as Documents.
|
|
175
|
+
|
|
176
|
+
Every item is one insider event (a filing's grouped buy/sell transactions)
|
|
177
|
+
with a structured ``meta["insider"]`` block: side, shares, average price,
|
|
178
|
+
total value, who traded, and whether it was a pre-planned 10b5-1 sale.
|
|
179
|
+
Relevance scores on this feed are deterministic from the event's summed
|
|
180
|
+
value, so ``min_relevance`` works as an "only large trades" dial.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from alphai_haystack import AlphaAIInsiderNewsFetcher
|
|
184
|
+
|
|
185
|
+
fetcher = AlphaAIInsiderNewsFetcher(min_relevance=7)
|
|
186
|
+
documents = fetcher.run()["documents"]
|
|
187
|
+
```
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
api_key: Secret = Secret.from_env_var("ALPHAI_API_KEY"),
|
|
193
|
+
symbol: str | None = None,
|
|
194
|
+
min_relevance: int | None = None,
|
|
195
|
+
top_k: int = DEFAULT_TOP_K,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""
|
|
198
|
+
:param api_key: AlphaAI API key. Defaults to the ``ALPHAI_API_KEY`` env var.
|
|
199
|
+
:param symbol: Only events for this ticker (share-class siblings included).
|
|
200
|
+
:param min_relevance: 1-10 floor; higher means larger trades only.
|
|
201
|
+
:param top_k: Maximum number of Documents to return per run.
|
|
202
|
+
"""
|
|
203
|
+
if top_k < 1:
|
|
204
|
+
raise ValueError("top_k must be at least 1")
|
|
205
|
+
self.api_key = api_key
|
|
206
|
+
self.symbol = symbol
|
|
207
|
+
self.min_relevance = min_relevance
|
|
208
|
+
self.top_k = top_k
|
|
209
|
+
self._client: Client | None = None
|
|
210
|
+
|
|
211
|
+
def warm_up(self) -> None:
|
|
212
|
+
"""Create the underlying API client once."""
|
|
213
|
+
if self._client is None:
|
|
214
|
+
self._client = Client(api_key=self.api_key.resolve_value())
|
|
215
|
+
|
|
216
|
+
def to_dict(self) -> dict[str, Any]:
|
|
217
|
+
return default_to_dict(
|
|
218
|
+
self,
|
|
219
|
+
api_key=self.api_key.to_dict(),
|
|
220
|
+
symbol=self.symbol,
|
|
221
|
+
min_relevance=self.min_relevance,
|
|
222
|
+
top_k=self.top_k,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def from_dict(cls, data: dict[str, Any]) -> AlphaAIInsiderNewsFetcher:
|
|
227
|
+
deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
|
|
228
|
+
result: AlphaAIInsiderNewsFetcher = default_from_dict(cls, data)
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
@component.output_types(documents=list[Document])
|
|
232
|
+
def run(
|
|
233
|
+
self,
|
|
234
|
+
symbol: str | None = None,
|
|
235
|
+
min_relevance: int | None = None,
|
|
236
|
+
top_k: int | None = None,
|
|
237
|
+
) -> dict[str, list[Document]]:
|
|
238
|
+
"""Fetch the newest matching insider events.
|
|
239
|
+
|
|
240
|
+
:param symbol: Overrides the ticker filter set in ``__init__``.
|
|
241
|
+
:param min_relevance: Overrides the relevance floor set in ``__init__``.
|
|
242
|
+
:param top_k: Overrides the maximum number of Documents.
|
|
243
|
+
:returns: ``{"documents": [...]}`` — newest first.
|
|
244
|
+
"""
|
|
245
|
+
self.warm_up()
|
|
246
|
+
assert self._client is not None
|
|
247
|
+
articles = self._client.news.insider_iter(
|
|
248
|
+
symbol=symbol if symbol is not None else self.symbol,
|
|
249
|
+
min_relevance=min_relevance if min_relevance is not None else self.min_relevance,
|
|
250
|
+
max_items=top_k if top_k is not None else self.top_k,
|
|
251
|
+
)
|
|
252
|
+
return {"documents": [_to_document(article) for article in articles]}
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from datetime import date, datetime, timezone
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from alphai.models.news import (
|
|
10
|
+
EnrichedArticle,
|
|
11
|
+
InsiderEvent,
|
|
12
|
+
OriginalArticle,
|
|
13
|
+
RichNewsArticle,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def make_article(
|
|
18
|
+
uid: str = "a1b2c3d4e5f60718",
|
|
19
|
+
title: str = "Nvidia beats on data-center revenue",
|
|
20
|
+
summary: str = "Q2 revenue came in above consensus, driven by data-center demand.",
|
|
21
|
+
tickers: list[str] | None = None,
|
|
22
|
+
relevance_score: int = 8,
|
|
23
|
+
category: str = "earnings",
|
|
24
|
+
insider: InsiderEvent | None = None,
|
|
25
|
+
sources_count: int | None = None,
|
|
26
|
+
) -> RichNewsArticle:
|
|
27
|
+
return RichNewsArticle(
|
|
28
|
+
original=OriginalArticle(
|
|
29
|
+
uid=uid,
|
|
30
|
+
title=title,
|
|
31
|
+
url=f"https://example.com/{uid}",
|
|
32
|
+
time_published=datetime(2026, 7, 28, 12, 30, tzinfo=timezone.utc),
|
|
33
|
+
summary=summary,
|
|
34
|
+
source="Example Wire",
|
|
35
|
+
source_domain="example.com",
|
|
36
|
+
),
|
|
37
|
+
enrichment=EnrichedArticle(
|
|
38
|
+
category=category,
|
|
39
|
+
tickers=tickers if tickers is not None else ["NVDA"],
|
|
40
|
+
relevance_score=relevance_score,
|
|
41
|
+
),
|
|
42
|
+
insider=insider,
|
|
43
|
+
sources_count=sources_count,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def make_insider_event() -> InsiderEvent:
|
|
48
|
+
return InsiderEvent(
|
|
49
|
+
side="sell",
|
|
50
|
+
transaction_code="S",
|
|
51
|
+
shares=Decimal("120000"),
|
|
52
|
+
avg_price_usd=Decimal("171.31"),
|
|
53
|
+
total_value_usd=Decimal("20557200.00"),
|
|
54
|
+
is_10b5_1=True,
|
|
55
|
+
insider_name="Jensen Huang",
|
|
56
|
+
insider_title="CEO",
|
|
57
|
+
is_officer=True,
|
|
58
|
+
transaction_date=date(2026, 7, 24),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FakeNewsResource:
|
|
63
|
+
"""Records call kwargs and yields canned articles."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, articles: list[RichNewsArticle]) -> None:
|
|
66
|
+
self.articles = articles
|
|
67
|
+
self.iter_calls: list[dict[str, Any]] = []
|
|
68
|
+
self.insider_calls: list[dict[str, Any]] = []
|
|
69
|
+
|
|
70
|
+
def iter(self, **kwargs: Any) -> Iterator[RichNewsArticle]:
|
|
71
|
+
self.iter_calls.append(kwargs)
|
|
72
|
+
max_items = kwargs.get("max_items")
|
|
73
|
+
yield from self.articles[:max_items]
|
|
74
|
+
|
|
75
|
+
def insider_iter(self, **kwargs: Any) -> Iterator[RichNewsArticle]:
|
|
76
|
+
self.insider_calls.append(kwargs)
|
|
77
|
+
max_items = kwargs.get("max_items")
|
|
78
|
+
yield from self.articles[:max_items]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FakeClient:
|
|
82
|
+
def __init__(self, articles: list[RichNewsArticle]) -> None:
|
|
83
|
+
self.news = FakeNewsResource(articles)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture()
|
|
87
|
+
def articles() -> list[RichNewsArticle]:
|
|
88
|
+
return [make_article(uid=f"uid{i:013d}", title=f"Article {i}") for i in range(25)]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from alphai.models.news import RichNewsArticle
|
|
5
|
+
from haystack import Document
|
|
6
|
+
|
|
7
|
+
from alphai_haystack import AlphaAIInsiderNewsFetcher, AlphaAINewsFetcher
|
|
8
|
+
from alphai_haystack.fetchers import _to_document
|
|
9
|
+
from conftest import FakeClient, make_article, make_insider_event
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def news_fetcher_with(articles: list[RichNewsArticle], **kwargs: object) -> AlphaAINewsFetcher:
|
|
13
|
+
fetcher = AlphaAINewsFetcher(**kwargs) # type: ignore[arg-type]
|
|
14
|
+
fetcher._client = FakeClient(articles) # type: ignore[assignment]
|
|
15
|
+
return fetcher
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def insider_fetcher_with(
|
|
19
|
+
articles: list[RichNewsArticle], **kwargs: object
|
|
20
|
+
) -> AlphaAIInsiderNewsFetcher:
|
|
21
|
+
fetcher = AlphaAIInsiderNewsFetcher(**kwargs) # type: ignore[arg-type]
|
|
22
|
+
fetcher._client = FakeClient(articles) # type: ignore[assignment]
|
|
23
|
+
return fetcher
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_document_mapping() -> None:
|
|
27
|
+
document = _to_document(make_article())
|
|
28
|
+
assert isinstance(document, Document)
|
|
29
|
+
assert document.content is not None
|
|
30
|
+
assert document.content.startswith("Nvidia beats on data-center revenue")
|
|
31
|
+
assert "above consensus" in document.content
|
|
32
|
+
assert document.meta["uid"] == "a1b2c3d4e5f60718"
|
|
33
|
+
assert document.meta["tickers"] == ["NVDA"]
|
|
34
|
+
assert document.meta["category"] == "earnings"
|
|
35
|
+
assert document.meta["relevance_score"] == 8
|
|
36
|
+
assert document.meta["published_at"] == "2026-07-28T12:30:00+00:00"
|
|
37
|
+
assert document.meta["source_domain"] == "example.com"
|
|
38
|
+
assert "insider" not in document.meta
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_document_mapping_insider_block() -> None:
|
|
42
|
+
document = _to_document(make_article(insider=make_insider_event()))
|
|
43
|
+
insider = document.meta["insider"]
|
|
44
|
+
assert insider["side"] == "sell"
|
|
45
|
+
assert insider["shares"] == "120000"
|
|
46
|
+
assert insider["total_value_usd"] == "20557200.00"
|
|
47
|
+
assert insider["is_10b5_1"] is True
|
|
48
|
+
assert insider["insider_name"] == "Jensen Huang"
|
|
49
|
+
assert insider["transaction_date"] == "2026-07-24"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_news_run_returns_top_k_documents(articles: list[RichNewsArticle]) -> None:
|
|
53
|
+
fetcher = news_fetcher_with(articles, top_k=5)
|
|
54
|
+
documents = fetcher.run()["documents"]
|
|
55
|
+
assert len(documents) == 5
|
|
56
|
+
assert documents[0].meta["title"] == "Article 0"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_news_run_passes_init_filters(articles: list[RichNewsArticle]) -> None:
|
|
60
|
+
fetcher = news_fetcher_with(
|
|
61
|
+
articles, symbol="NVDA", category="earnings", min_relevance=7, collapse_stories=True
|
|
62
|
+
)
|
|
63
|
+
fetcher.run()
|
|
64
|
+
call = fetcher._client.news.iter_calls[0] # type: ignore[union-attr]
|
|
65
|
+
assert call == {
|
|
66
|
+
"symbol": "NVDA",
|
|
67
|
+
"category": "earnings",
|
|
68
|
+
"min_relevance": 7,
|
|
69
|
+
"collapse_stories": True,
|
|
70
|
+
"max_items": 10,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_news_run_overrides_beat_init(articles: list[RichNewsArticle]) -> None:
|
|
75
|
+
fetcher = news_fetcher_with(articles, symbol="NVDA", min_relevance=7, top_k=10)
|
|
76
|
+
fetcher.run(symbol="AMD", min_relevance=9, top_k=3)
|
|
77
|
+
call = fetcher._client.news.iter_calls[0] # type: ignore[union-attr]
|
|
78
|
+
assert call["symbol"] == "AMD"
|
|
79
|
+
assert call["min_relevance"] == 9
|
|
80
|
+
assert call["max_items"] == 3
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_insider_run_passes_filters(articles: list[RichNewsArticle]) -> None:
|
|
84
|
+
fetcher = insider_fetcher_with(articles, symbol="NVDA", min_relevance=8, top_k=4)
|
|
85
|
+
documents = fetcher.run()["documents"]
|
|
86
|
+
assert len(documents) == 4
|
|
87
|
+
call = fetcher._client.news.insider_calls[0] # type: ignore[union-attr]
|
|
88
|
+
assert call == {"symbol": "NVDA", "min_relevance": 8, "max_items": 4}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_top_k_must_be_positive() -> None:
|
|
92
|
+
with pytest.raises(ValueError):
|
|
93
|
+
AlphaAINewsFetcher(top_k=0)
|
|
94
|
+
with pytest.raises(ValueError):
|
|
95
|
+
AlphaAIInsiderNewsFetcher(top_k=-1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_warm_up_uses_resolved_secret(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
99
|
+
created: list[str] = []
|
|
100
|
+
|
|
101
|
+
class RecordingClient:
|
|
102
|
+
def __init__(self, api_key: str) -> None:
|
|
103
|
+
created.append(api_key)
|
|
104
|
+
|
|
105
|
+
monkeypatch.setattr("alphai_haystack.fetchers.Client", RecordingClient)
|
|
106
|
+
monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
|
|
107
|
+
fetcher = AlphaAINewsFetcher()
|
|
108
|
+
fetcher.warm_up()
|
|
109
|
+
fetcher.warm_up() # second call must not create a second client
|
|
110
|
+
assert created == ["ak_test_123"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from alphai_haystack import AlphaAIInsiderNewsFetcher, AlphaAINewsFetcher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_news_fetcher_to_dict_from_dict_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
9
|
+
monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
|
|
10
|
+
fetcher = AlphaAINewsFetcher(
|
|
11
|
+
symbol="NVDA", category="earnings", min_relevance=7, collapse_stories=True, top_k=5
|
|
12
|
+
)
|
|
13
|
+
data = fetcher.to_dict()
|
|
14
|
+
|
|
15
|
+
init = data["init_parameters"]
|
|
16
|
+
assert init["symbol"] == "NVDA"
|
|
17
|
+
assert init["category"] == "earnings"
|
|
18
|
+
assert init["min_relevance"] == 7
|
|
19
|
+
assert init["collapse_stories"] is True
|
|
20
|
+
assert init["top_k"] == 5
|
|
21
|
+
# The secret must serialize as an env-var reference, never as the raw key.
|
|
22
|
+
assert init["api_key"]["type"] == "env_var"
|
|
23
|
+
assert "ak_test_123" not in str(data)
|
|
24
|
+
|
|
25
|
+
restored = AlphaAINewsFetcher.from_dict(data)
|
|
26
|
+
assert restored.symbol == "NVDA"
|
|
27
|
+
assert restored.top_k == 5
|
|
28
|
+
assert restored.api_key.resolve_value() == "ak_test_123"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_insider_fetcher_to_dict_from_dict_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
32
|
+
monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
|
|
33
|
+
fetcher = AlphaAIInsiderNewsFetcher(symbol="AAPL", min_relevance=9, top_k=3)
|
|
34
|
+
data = fetcher.to_dict()
|
|
35
|
+
|
|
36
|
+
init = data["init_parameters"]
|
|
37
|
+
assert init["symbol"] == "AAPL"
|
|
38
|
+
assert init["min_relevance"] == 9
|
|
39
|
+
assert init["top_k"] == 3
|
|
40
|
+
assert init["api_key"]["type"] == "env_var"
|
|
41
|
+
|
|
42
|
+
restored = AlphaAIInsiderNewsFetcher.from_dict(data)
|
|
43
|
+
assert restored.symbol == "AAPL"
|
|
44
|
+
assert restored.min_relevance == 9
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_fetchers_are_pipeline_serializable(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
48
|
+
"""The components must survive a full Pipeline YAML round-trip."""
|
|
49
|
+
from haystack import Pipeline
|
|
50
|
+
|
|
51
|
+
monkeypatch.setenv("ALPHAI_API_KEY", "ak_test_123")
|
|
52
|
+
pipeline = Pipeline()
|
|
53
|
+
pipeline.add_component("news", AlphaAINewsFetcher(symbol="NVDA"))
|
|
54
|
+
pipeline.add_component("insider", AlphaAIInsiderNewsFetcher(min_relevance=8))
|
|
55
|
+
|
|
56
|
+
yaml_text = pipeline.dumps()
|
|
57
|
+
assert "ak_test_123" not in yaml_text
|
|
58
|
+
|
|
59
|
+
restored = Pipeline.loads(yaml_text)
|
|
60
|
+
news = restored.get_component("news")
|
|
61
|
+
assert isinstance(news, AlphaAINewsFetcher)
|
|
62
|
+
assert news.symbol == "NVDA"
|