vantafin 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.
- vantafin-0.1.0/.gitignore +13 -0
- vantafin-0.1.0/LICENSE +19 -0
- vantafin-0.1.0/PKG-INFO +181 -0
- vantafin-0.1.0/README.md +150 -0
- vantafin-0.1.0/pyproject.toml +52 -0
- vantafin-0.1.0/src/vantafin/__init__.py +35 -0
- vantafin-0.1.0/src/vantafin/client.py +376 -0
- vantafin-0.1.0/src/vantafin/exceptions.py +38 -0
- vantafin-0.1.0/src/vantafin/ws.py +105 -0
vantafin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Vantafin
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
vantafin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vantafin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Vantafin REST API (market data, fundamentals, SEC filings, news and more).
|
|
5
|
+
Project-URL: Homepage, https://vantafin.com
|
|
6
|
+
Project-URL: Documentation, https://vantafin.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/vantafin/vantafin
|
|
8
|
+
Project-URL: Issues, https://github.com/vantafin/vantafin/issues
|
|
9
|
+
Author: Vantafin
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,finance,market-data,sec,stocks,vantafin
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Requires-Dist: requests>=2.25
|
|
28
|
+
Provides-Extra: websocket
|
|
29
|
+
Requires-Dist: websocket-client>=1.6; extra == 'websocket'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# vantafin
|
|
33
|
+
|
|
34
|
+
Official Python client for the [Vantafin](https://vantafin.com) REST API - quotes,
|
|
35
|
+
corporate actions, fundamentals, SEC filings, news, earnings transcripts, and
|
|
36
|
+
earnings materials (IR decks, press releases, supplements).
|
|
37
|
+
|
|
38
|
+
**Source:** [github.com/vantafin/vantafin](https://github.com/vantafin/vantafin)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install vantafin
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install git+https://github.com/vantafin/vantafin.git
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Authentication
|
|
53
|
+
|
|
54
|
+
Grab your API key from **Settings → API Usage** at
|
|
55
|
+
[vantafin.com/app/settings](https://vantafin.com/app/settings). Keys look like
|
|
56
|
+
`vf-live-...`. You can reset a key at any time from the same page.
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from vantafin import RESTClient
|
|
62
|
+
|
|
63
|
+
client = RESTClient("vf-live-your_api_key")
|
|
64
|
+
|
|
65
|
+
# Latest quote
|
|
66
|
+
print(client.get_quote("AAPL"))
|
|
67
|
+
|
|
68
|
+
# Company profile
|
|
69
|
+
print(client.get_ticker("AAPL"))
|
|
70
|
+
|
|
71
|
+
# All tickers (paginated)
|
|
72
|
+
page = client.list_tickers(type="stock", limit=100)
|
|
73
|
+
print(page["count"], page["next_cursor"])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Available methods
|
|
77
|
+
|
|
78
|
+
| Method | Endpoint |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `get_status()` | `GET /status` |
|
|
81
|
+
| `list_tickers(...)` | `GET /companydata/tickers` |
|
|
82
|
+
| `get_ticker(ticker)` | `GET /companydata/tickers/{ticker}` |
|
|
83
|
+
| `get_company_by_cik(cik)` | `GET /companydata/ciks/{cik}` |
|
|
84
|
+
| `get_company_by_isin(isin)` | `GET /companydata/isins/{isin}` |
|
|
85
|
+
| `get_company_by_cusip(cusip)` | `GET /companydata/cusips/{cusip}` |
|
|
86
|
+
| `search(query, ...)` | `GET /companydata/search` |
|
|
87
|
+
| `get_quote(ticker)` | `GET /quotes/{ticker}` |
|
|
88
|
+
| `get_quotes(tickers)` | `GET /quotes` |
|
|
89
|
+
| `screener(...)` | `GET /screener` |
|
|
90
|
+
| `screener_fields()` | `GET /screener/fields` |
|
|
91
|
+
| `list_earnings(...)` | `GET /earnings` |
|
|
92
|
+
| `get_earnings(ticker, ...)` | `GET /earnings/{ticker}` |
|
|
93
|
+
| `list_dividends(...)` | `GET /dividends` |
|
|
94
|
+
| `get_dividends(ticker, ...)` | `GET /dividends/{ticker}` |
|
|
95
|
+
| `list_splits(...)` | `GET /splits` |
|
|
96
|
+
| `get_splits(ticker, ...)` | `GET /splits/{ticker}` |
|
|
97
|
+
| `list_ipos(...)` | `GET /ipos` |
|
|
98
|
+
| `get_ipos(ticker, ...)` | `GET /ipos/{ticker}` |
|
|
99
|
+
| `get_etf_holdings(ticker)` | `GET /etf-holdings/{ticker}` |
|
|
100
|
+
| `get_institutional_ownership(ticker, ...)` | `GET /institutional-ownership/{ticker}` |
|
|
101
|
+
| `get_fund_holdings(ticker, ...)` | `GET /fund-holdings/{ticker}` |
|
|
102
|
+
| `get_short_interest(ticker, ...)` | `GET /short-interest/{ticker}` |
|
|
103
|
+
| `list_insider_transactions(...)` | `GET /insider-transactions` |
|
|
104
|
+
| `get_insider_transactions(ticker, ...)` | `GET /insider-transactions/{ticker}` |
|
|
105
|
+
| `get_financials(ticker, ...)` | `GET /financials/{ticker}` |
|
|
106
|
+
| `get_revenue_segmentation(ticker, ...)` | `GET /revenue-segmentation/{ticker}` |
|
|
107
|
+
| `get_allocation_breakdown(ticker)` | `GET /allocation-breakdown/{ticker}` |
|
|
108
|
+
| `get_macro(...)` | `GET /macro` |
|
|
109
|
+
| `get_filings(ticker, ...)` | `GET /filings/{ticker}` |
|
|
110
|
+
| `get_filing(filing_id)` | `GET /filings/by-id/{filing_id}` |
|
|
111
|
+
| `get_news(...)` | `GET /news` |
|
|
112
|
+
| `get_ticker_news(ticker, ...)` | `GET /news/{ticker}` |
|
|
113
|
+
| `list_transcript_tickers(...)` | `GET /transcripts/tickers` |
|
|
114
|
+
| `list_transcripts(ticker, ...)` | `GET /transcripts/{ticker}` |
|
|
115
|
+
| `get_transcript(ticker, transcript_id)` | `GET /transcripts/{ticker}/{transcript_id}` |
|
|
116
|
+
| `list_earnings_materials(ticker, ...)` | `GET /earnings-materials/{ticker}` |
|
|
117
|
+
| `get_earnings_material(ticker, material_id)` | `GET /earnings-materials/{ticker}/{material_id}` |
|
|
118
|
+
|
|
119
|
+
## Rate limits
|
|
120
|
+
|
|
121
|
+
The API allows **1,000 requests per minute** per key. When you exceed it the
|
|
122
|
+
client raises `RateLimitError` (with `.retry_after` seconds). Every response
|
|
123
|
+
also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
|
|
124
|
+
headers.
|
|
125
|
+
|
|
126
|
+
## Error handling
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from vantafin import RESTClient, RateLimitError, NotFoundError
|
|
130
|
+
|
|
131
|
+
client = RESTClient("vf-live-your_api_key")
|
|
132
|
+
try:
|
|
133
|
+
client.get_quote("NOPE")
|
|
134
|
+
except NotFoundError:
|
|
135
|
+
print("Unknown ticker")
|
|
136
|
+
except RateLimitError as e:
|
|
137
|
+
print(f"Slow down, retry in {e.retry_after}s")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## WebSocket streaming
|
|
141
|
+
|
|
142
|
+
Stream real-time data over a single WebSocket connection to
|
|
143
|
+
`wss://socket.vantafin.com/v1/stocks`. Three channels are available:
|
|
144
|
+
|
|
145
|
+
| Channel | Subscribe with | Message `type` |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| Live news / press releases | `N.<ticker>` or `N.*` | `news` |
|
|
148
|
+
| Live SEC filings | `F.<ticker>` or `F.*` | `filing` |
|
|
149
|
+
| Trading halts | `H.<ticker>` or `H.*` | `halt` |
|
|
150
|
+
|
|
151
|
+
The WebSocket client needs the optional `websocket-client` dependency:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pip install vantafin[websocket]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from vantafin import WebSocketClient
|
|
159
|
+
|
|
160
|
+
ws = WebSocketClient("vf-live-your_api_key")
|
|
161
|
+
ws.connect()
|
|
162
|
+
ws.subscribe(["N.AAPL", "F.AAPL", "H.AAPL"])
|
|
163
|
+
|
|
164
|
+
for message in ws:
|
|
165
|
+
print(message)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Other languages
|
|
169
|
+
|
|
170
|
+
Official clients for other languages live alongside this Python package:
|
|
171
|
+
|
|
172
|
+
| Language | Folder |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| JavaScript / Node | [`javascript/`](./javascript) |
|
|
175
|
+
| Go | [`go/`](./go) |
|
|
176
|
+
| Java | [`java/`](./java) |
|
|
177
|
+
| Ruby | [`ruby/`](./ruby) |
|
|
178
|
+
|
|
179
|
+
## Full documentation
|
|
180
|
+
|
|
181
|
+
[https://vantafin.com/docs](https://vantafin.com/docs)
|
vantafin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# vantafin
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Vantafin](https://vantafin.com) REST API - quotes,
|
|
4
|
+
corporate actions, fundamentals, SEC filings, news, earnings transcripts, and
|
|
5
|
+
earnings materials (IR decks, press releases, supplements).
|
|
6
|
+
|
|
7
|
+
**Source:** [github.com/vantafin/vantafin](https://github.com/vantafin/vantafin)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install vantafin
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install from source:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install git+https://github.com/vantafin/vantafin.git
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
Grab your API key from **Settings → API Usage** at
|
|
24
|
+
[vantafin.com/app/settings](https://vantafin.com/app/settings). Keys look like
|
|
25
|
+
`vf-live-...`. You can reset a key at any time from the same page.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from vantafin import RESTClient
|
|
31
|
+
|
|
32
|
+
client = RESTClient("vf-live-your_api_key")
|
|
33
|
+
|
|
34
|
+
# Latest quote
|
|
35
|
+
print(client.get_quote("AAPL"))
|
|
36
|
+
|
|
37
|
+
# Company profile
|
|
38
|
+
print(client.get_ticker("AAPL"))
|
|
39
|
+
|
|
40
|
+
# All tickers (paginated)
|
|
41
|
+
page = client.list_tickers(type="stock", limit=100)
|
|
42
|
+
print(page["count"], page["next_cursor"])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Available methods
|
|
46
|
+
|
|
47
|
+
| Method | Endpoint |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `get_status()` | `GET /status` |
|
|
50
|
+
| `list_tickers(...)` | `GET /companydata/tickers` |
|
|
51
|
+
| `get_ticker(ticker)` | `GET /companydata/tickers/{ticker}` |
|
|
52
|
+
| `get_company_by_cik(cik)` | `GET /companydata/ciks/{cik}` |
|
|
53
|
+
| `get_company_by_isin(isin)` | `GET /companydata/isins/{isin}` |
|
|
54
|
+
| `get_company_by_cusip(cusip)` | `GET /companydata/cusips/{cusip}` |
|
|
55
|
+
| `search(query, ...)` | `GET /companydata/search` |
|
|
56
|
+
| `get_quote(ticker)` | `GET /quotes/{ticker}` |
|
|
57
|
+
| `get_quotes(tickers)` | `GET /quotes` |
|
|
58
|
+
| `screener(...)` | `GET /screener` |
|
|
59
|
+
| `screener_fields()` | `GET /screener/fields` |
|
|
60
|
+
| `list_earnings(...)` | `GET /earnings` |
|
|
61
|
+
| `get_earnings(ticker, ...)` | `GET /earnings/{ticker}` |
|
|
62
|
+
| `list_dividends(...)` | `GET /dividends` |
|
|
63
|
+
| `get_dividends(ticker, ...)` | `GET /dividends/{ticker}` |
|
|
64
|
+
| `list_splits(...)` | `GET /splits` |
|
|
65
|
+
| `get_splits(ticker, ...)` | `GET /splits/{ticker}` |
|
|
66
|
+
| `list_ipos(...)` | `GET /ipos` |
|
|
67
|
+
| `get_ipos(ticker, ...)` | `GET /ipos/{ticker}` |
|
|
68
|
+
| `get_etf_holdings(ticker)` | `GET /etf-holdings/{ticker}` |
|
|
69
|
+
| `get_institutional_ownership(ticker, ...)` | `GET /institutional-ownership/{ticker}` |
|
|
70
|
+
| `get_fund_holdings(ticker, ...)` | `GET /fund-holdings/{ticker}` |
|
|
71
|
+
| `get_short_interest(ticker, ...)` | `GET /short-interest/{ticker}` |
|
|
72
|
+
| `list_insider_transactions(...)` | `GET /insider-transactions` |
|
|
73
|
+
| `get_insider_transactions(ticker, ...)` | `GET /insider-transactions/{ticker}` |
|
|
74
|
+
| `get_financials(ticker, ...)` | `GET /financials/{ticker}` |
|
|
75
|
+
| `get_revenue_segmentation(ticker, ...)` | `GET /revenue-segmentation/{ticker}` |
|
|
76
|
+
| `get_allocation_breakdown(ticker)` | `GET /allocation-breakdown/{ticker}` |
|
|
77
|
+
| `get_macro(...)` | `GET /macro` |
|
|
78
|
+
| `get_filings(ticker, ...)` | `GET /filings/{ticker}` |
|
|
79
|
+
| `get_filing(filing_id)` | `GET /filings/by-id/{filing_id}` |
|
|
80
|
+
| `get_news(...)` | `GET /news` |
|
|
81
|
+
| `get_ticker_news(ticker, ...)` | `GET /news/{ticker}` |
|
|
82
|
+
| `list_transcript_tickers(...)` | `GET /transcripts/tickers` |
|
|
83
|
+
| `list_transcripts(ticker, ...)` | `GET /transcripts/{ticker}` |
|
|
84
|
+
| `get_transcript(ticker, transcript_id)` | `GET /transcripts/{ticker}/{transcript_id}` |
|
|
85
|
+
| `list_earnings_materials(ticker, ...)` | `GET /earnings-materials/{ticker}` |
|
|
86
|
+
| `get_earnings_material(ticker, material_id)` | `GET /earnings-materials/{ticker}/{material_id}` |
|
|
87
|
+
|
|
88
|
+
## Rate limits
|
|
89
|
+
|
|
90
|
+
The API allows **1,000 requests per minute** per key. When you exceed it the
|
|
91
|
+
client raises `RateLimitError` (with `.retry_after` seconds). Every response
|
|
92
|
+
also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
|
|
93
|
+
headers.
|
|
94
|
+
|
|
95
|
+
## Error handling
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from vantafin import RESTClient, RateLimitError, NotFoundError
|
|
99
|
+
|
|
100
|
+
client = RESTClient("vf-live-your_api_key")
|
|
101
|
+
try:
|
|
102
|
+
client.get_quote("NOPE")
|
|
103
|
+
except NotFoundError:
|
|
104
|
+
print("Unknown ticker")
|
|
105
|
+
except RateLimitError as e:
|
|
106
|
+
print(f"Slow down, retry in {e.retry_after}s")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## WebSocket streaming
|
|
110
|
+
|
|
111
|
+
Stream real-time data over a single WebSocket connection to
|
|
112
|
+
`wss://socket.vantafin.com/v1/stocks`. Three channels are available:
|
|
113
|
+
|
|
114
|
+
| Channel | Subscribe with | Message `type` |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| Live news / press releases | `N.<ticker>` or `N.*` | `news` |
|
|
117
|
+
| Live SEC filings | `F.<ticker>` or `F.*` | `filing` |
|
|
118
|
+
| Trading halts | `H.<ticker>` or `H.*` | `halt` |
|
|
119
|
+
|
|
120
|
+
The WebSocket client needs the optional `websocket-client` dependency:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install vantafin[websocket]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from vantafin import WebSocketClient
|
|
128
|
+
|
|
129
|
+
ws = WebSocketClient("vf-live-your_api_key")
|
|
130
|
+
ws.connect()
|
|
131
|
+
ws.subscribe(["N.AAPL", "F.AAPL", "H.AAPL"])
|
|
132
|
+
|
|
133
|
+
for message in ws:
|
|
134
|
+
print(message)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Other languages
|
|
138
|
+
|
|
139
|
+
Official clients for other languages live alongside this Python package:
|
|
140
|
+
|
|
141
|
+
| Language | Folder |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| JavaScript / Node | [`javascript/`](./javascript) |
|
|
144
|
+
| Go | [`go/`](./go) |
|
|
145
|
+
| Java | [`java/`](./java) |
|
|
146
|
+
| Ruby | [`ruby/`](./ruby) |
|
|
147
|
+
|
|
148
|
+
## Full documentation
|
|
149
|
+
|
|
150
|
+
[https://vantafin.com/docs](https://vantafin.com/docs)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vantafin"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Vantafin" },
|
|
10
|
+
]
|
|
11
|
+
description = "Official Python client for the Vantafin REST API (market data, fundamentals, SEC filings, news and more)."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
keywords = ["vantafin", "stocks", "market-data", "finance", "sec", "api"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Topic :: Office/Business :: Financial",
|
|
31
|
+
]
|
|
32
|
+
dependencies = ["requests>=2.25"]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
websocket = ["websocket-client>=1.6"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://vantafin.com"
|
|
39
|
+
Documentation = "https://vantafin.com/docs"
|
|
40
|
+
Repository = "https://github.com/vantafin/vantafin"
|
|
41
|
+
Issues = "https://github.com/vantafin/vantafin/issues"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/vantafin"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = [
|
|
48
|
+
"/src/vantafin",
|
|
49
|
+
"/LICENSE",
|
|
50
|
+
"/README.md",
|
|
51
|
+
"/pyproject.toml",
|
|
52
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vantafin - official Python client for the Vantafin REST API.
|
|
3
|
+
|
|
4
|
+
from vantafin import RESTClient
|
|
5
|
+
|
|
6
|
+
client = RESTClient("vf-live-your_api_key")
|
|
7
|
+
print(client.get_quote("AAPL"))
|
|
8
|
+
|
|
9
|
+
Docs: https://vantafin.com/docs
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .client import RESTClient
|
|
13
|
+
from .ws import WebSocketClient, DEFAULT_WS_URL
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
VantafinError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
BadRequestError,
|
|
20
|
+
ServerError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"RESTClient",
|
|
27
|
+
"WebSocketClient",
|
|
28
|
+
"DEFAULT_WS_URL",
|
|
29
|
+
"VantafinError",
|
|
30
|
+
"AuthenticationError",
|
|
31
|
+
"RateLimitError",
|
|
32
|
+
"NotFoundError",
|
|
33
|
+
"BadRequestError",
|
|
34
|
+
"ServerError",
|
|
35
|
+
]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Synchronous REST client for the Vantafin Data API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
BadRequestError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
VantafinError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
DEFAULT_BASE_URL = "https://api.vantafin.com/v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RESTClient:
|
|
22
|
+
"""
|
|
23
|
+
Client for the Vantafin REST API.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key: Your Vantafin API key (``vf-live-...``). Find or reset it in
|
|
27
|
+
Settings → API Usage at https://vantafin.com/app/settings.
|
|
28
|
+
base_url: Override the API base URL (defaults to the production API).
|
|
29
|
+
timeout: Per-request timeout in seconds.
|
|
30
|
+
session: Optionally supply a pre-configured ``requests.Session``.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> from vantafin import RESTClient
|
|
34
|
+
>>> client = RESTClient("vf-live-...")
|
|
35
|
+
>>> client.get_quote("AAPL")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: str,
|
|
41
|
+
*,
|
|
42
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
43
|
+
timeout: float = 30.0,
|
|
44
|
+
session: Optional[requests.Session] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
if not api_key or not isinstance(api_key, str):
|
|
47
|
+
raise ValueError("api_key is required (e.g. 'vf-live-...').")
|
|
48
|
+
self.api_key = api_key.strip()
|
|
49
|
+
self.base_url = base_url.rstrip("/")
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self._session = session or requests.Session()
|
|
52
|
+
self._session.headers.update(
|
|
53
|
+
{
|
|
54
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
55
|
+
"Accept": "application/json",
|
|
56
|
+
"User-Agent": "vantafin-python/0.1.0",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ── core request ────────────────────────────────────────────────────
|
|
61
|
+
def _get(self, path: str, params: Optional[dict] = None) -> Any:
|
|
62
|
+
url = f"{self.base_url}{path}"
|
|
63
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None}
|
|
64
|
+
resp = self._session.get(url, params=clean, timeout=self.timeout)
|
|
65
|
+
return self._handle(resp)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _handle(resp: requests.Response) -> Any:
|
|
69
|
+
if resp.status_code < 400:
|
|
70
|
+
try:
|
|
71
|
+
return resp.json()
|
|
72
|
+
except ValueError:
|
|
73
|
+
return resp.text
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
payload = resp.json()
|
|
77
|
+
message = payload.get("error") or payload.get("detail") or resp.text
|
|
78
|
+
except ValueError:
|
|
79
|
+
payload = None
|
|
80
|
+
message = resp.text or resp.reason
|
|
81
|
+
|
|
82
|
+
code = resp.status_code
|
|
83
|
+
if code == 401:
|
|
84
|
+
raise AuthenticationError(message, status_code=code, response=resp)
|
|
85
|
+
if code == 400:
|
|
86
|
+
raise BadRequestError(message, status_code=code, response=resp)
|
|
87
|
+
if code == 404:
|
|
88
|
+
raise NotFoundError(message, status_code=code, response=resp)
|
|
89
|
+
if code == 429:
|
|
90
|
+
retry_after = None
|
|
91
|
+
if isinstance(payload, dict):
|
|
92
|
+
retry_after = payload.get("retry_after_seconds")
|
|
93
|
+
if retry_after is None:
|
|
94
|
+
hdr = resp.headers.get("Retry-After")
|
|
95
|
+
retry_after = int(hdr) if hdr and hdr.isdigit() else None
|
|
96
|
+
raise RateLimitError(
|
|
97
|
+
message, retry_after=retry_after, status_code=code, response=resp
|
|
98
|
+
)
|
|
99
|
+
if code >= 500:
|
|
100
|
+
raise ServerError(message, status_code=code, response=resp)
|
|
101
|
+
raise VantafinError(message, status_code=code, response=resp)
|
|
102
|
+
|
|
103
|
+
# ── status ──────────────────────────────────────────────────────────
|
|
104
|
+
def get_status(self) -> dict:
|
|
105
|
+
"""Verify the API key and read your rate-limit ceiling."""
|
|
106
|
+
return self._get("/status")
|
|
107
|
+
|
|
108
|
+
# ── company data ───────────────────────────────────────────────────────
|
|
109
|
+
def list_tickers(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
search: Optional[str] = None,
|
|
113
|
+
type: Optional[str] = None,
|
|
114
|
+
cursor: Optional[str] = None,
|
|
115
|
+
limit: Optional[int] = None,
|
|
116
|
+
) -> dict:
|
|
117
|
+
"""List the ticker universe. Paginate via the returned ``next_cursor``."""
|
|
118
|
+
return self._get(
|
|
119
|
+
"/companydata/tickers",
|
|
120
|
+
{"search": search, "type": type, "cursor": cursor, "limit": limit},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def get_ticker(self, ticker: str) -> dict:
|
|
124
|
+
"""Full company/ETF profile for a single ticker."""
|
|
125
|
+
return self._get(f"/companydata/tickers/{ticker.upper()}")
|
|
126
|
+
|
|
127
|
+
def get_company_by_cik(self, cik: str) -> dict:
|
|
128
|
+
"""Full company/ETF profile for a single SEC CIK."""
|
|
129
|
+
return self._get(f"/companydata/ciks/{cik.strip()}")
|
|
130
|
+
|
|
131
|
+
def get_company_by_isin(self, isin: str) -> dict:
|
|
132
|
+
"""Full company/ETF profile for a single ISIN."""
|
|
133
|
+
return self._get(f"/companydata/isins/{isin.strip().upper()}")
|
|
134
|
+
|
|
135
|
+
def get_company_by_cusip(self, cusip: str) -> dict:
|
|
136
|
+
"""Full company/ETF profile for a single CUSIP."""
|
|
137
|
+
return self._get(f"/companydata/cusips/{cusip.strip().upper()}")
|
|
138
|
+
|
|
139
|
+
def search(self, query: str, *, limit: Optional[int] = None) -> dict:
|
|
140
|
+
"""Fuzzy search across tickers and company names."""
|
|
141
|
+
return self._get("/companydata/search", {"query": query, "limit": limit})
|
|
142
|
+
|
|
143
|
+
# ── market data ─────────────────────────────────────────────────────
|
|
144
|
+
def get_quote(self, ticker: str) -> dict:
|
|
145
|
+
"""Latest stored quote for a ticker."""
|
|
146
|
+
return self._get(f"/quotes/{ticker.upper()}")
|
|
147
|
+
|
|
148
|
+
def get_quotes(self, tickers: Union[str, Sequence[str]]) -> dict:
|
|
149
|
+
"""Latest quotes for up to 200 tickers at once."""
|
|
150
|
+
if not isinstance(tickers, str):
|
|
151
|
+
tickers = ",".join(s.upper() for s in tickers)
|
|
152
|
+
return self._get("/quotes", {"tickers": tickers})
|
|
153
|
+
|
|
154
|
+
# ── screener ────────────────────────────────────────────────────────
|
|
155
|
+
def screener(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
filter: Optional[Union[str, Sequence[str]]] = None,
|
|
159
|
+
sort: Optional[str] = None,
|
|
160
|
+
order: Optional[str] = None,
|
|
161
|
+
columns: Optional[Union[str, Sequence[str]]] = None,
|
|
162
|
+
limit: Optional[int] = None,
|
|
163
|
+
offset: Optional[int] = None,
|
|
164
|
+
) -> dict:
|
|
165
|
+
"""Screen the equity universe across the full metric registry.
|
|
166
|
+
|
|
167
|
+
``filter`` is one or more ``metric:operator:value`` clauses (a string or a list
|
|
168
|
+
of strings), combined with AND, e.g. ``["peRatio:below:20", "marketCap:above:1e9"]``.
|
|
169
|
+
Sort by any metric id, page with ``limit``/``offset`` and request extra metric
|
|
170
|
+
``columns``. Call :meth:`screener_fields` for the available metrics.
|
|
171
|
+
"""
|
|
172
|
+
cols = ",".join(columns) if isinstance(columns, (list, tuple)) else columns
|
|
173
|
+
flt = list(filter) if isinstance(filter, (list, tuple)) else filter
|
|
174
|
+
return self._get(
|
|
175
|
+
"/screener",
|
|
176
|
+
{
|
|
177
|
+
"filter": flt,
|
|
178
|
+
"sort": sort,
|
|
179
|
+
"order": order,
|
|
180
|
+
"columns": cols,
|
|
181
|
+
"limit": limit,
|
|
182
|
+
"offset": offset,
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def screener_fields(self) -> dict:
|
|
187
|
+
"""List every metric available to filter, sort or select in the screener."""
|
|
188
|
+
return self._get("/screener/fields")
|
|
189
|
+
|
|
190
|
+
# ── corporate actions ───────────────────────────────────────────────
|
|
191
|
+
def list_earnings(self, *, page: Optional[int] = None, limit: Optional[int] = None) -> dict:
|
|
192
|
+
return self._get("/earnings", {"page": page, "limit": limit})
|
|
193
|
+
|
|
194
|
+
def get_earnings(self, ticker: str, *, limit: Optional[int] = None) -> dict:
|
|
195
|
+
return self._get(f"/earnings/{ticker.upper()}", {"limit": limit})
|
|
196
|
+
|
|
197
|
+
def list_dividends(self, *, page: Optional[int] = None, limit: Optional[int] = None) -> dict:
|
|
198
|
+
return self._get("/dividends", {"page": page, "limit": limit})
|
|
199
|
+
|
|
200
|
+
def get_dividends(self, ticker: str, *, limit: Optional[int] = None) -> dict:
|
|
201
|
+
return self._get(f"/dividends/{ticker.upper()}", {"limit": limit})
|
|
202
|
+
|
|
203
|
+
def list_splits(self, *, page: Optional[int] = None, limit: Optional[int] = None) -> dict:
|
|
204
|
+
return self._get("/splits", {"page": page, "limit": limit})
|
|
205
|
+
|
|
206
|
+
def get_splits(self, ticker: str, *, limit: Optional[int] = None) -> dict:
|
|
207
|
+
return self._get(f"/splits/{ticker.upper()}", {"limit": limit})
|
|
208
|
+
|
|
209
|
+
def list_ipos(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
from_: Optional[str] = None,
|
|
213
|
+
to: Optional[str] = None,
|
|
214
|
+
page: Optional[int] = None,
|
|
215
|
+
limit: Optional[int] = None,
|
|
216
|
+
) -> dict:
|
|
217
|
+
"""IPO calendar (past + upcoming) across all tickers, newest first."""
|
|
218
|
+
return self._get("/ipos", {"from": from_, "to": to, "page": page, "limit": limit})
|
|
219
|
+
|
|
220
|
+
def get_ipos(self, ticker: str, *, limit: Optional[int] = None) -> dict:
|
|
221
|
+
"""Merged IPO record(s) for a single ticker."""
|
|
222
|
+
return self._get(f"/ipos/{ticker.upper()}", {"limit": limit})
|
|
223
|
+
|
|
224
|
+
# ── ownership ───────────────────────────────────────────────────────
|
|
225
|
+
def get_etf_holdings(self, ticker: str) -> dict:
|
|
226
|
+
"""Constituent holdings for an ETF or mutual fund."""
|
|
227
|
+
return self._get(f"/etf-holdings/{ticker.upper()}")
|
|
228
|
+
|
|
229
|
+
def get_institutional_ownership(
|
|
230
|
+
self, ticker: str, *, limit: Optional[int] = None
|
|
231
|
+
) -> dict:
|
|
232
|
+
return self._get(
|
|
233
|
+
f"/institutional-ownership/{ticker.upper()}", {"limit": limit}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def get_fund_holdings(self, ticker: str, *, limit: Optional[int] = None) -> dict:
|
|
237
|
+
return self._get(f"/fund-holdings/{ticker.upper()}", {"limit": limit})
|
|
238
|
+
|
|
239
|
+
def get_short_interest(self, ticker: str, *, limit: Optional[int] = None) -> dict:
|
|
240
|
+
return self._get(f"/short-interest/{ticker.upper()}", {"limit": limit})
|
|
241
|
+
|
|
242
|
+
def list_insider_transactions(
|
|
243
|
+
self, *, page: Optional[int] = None, limit: Optional[int] = None
|
|
244
|
+
) -> dict:
|
|
245
|
+
return self._get("/insider-transactions", {"page": page, "limit": limit})
|
|
246
|
+
|
|
247
|
+
def get_insider_transactions(
|
|
248
|
+
self, ticker: str, *, page: Optional[int] = None, limit: Optional[int] = None
|
|
249
|
+
) -> dict:
|
|
250
|
+
return self._get(
|
|
251
|
+
f"/insider-transactions/{ticker.upper()}", {"page": page, "limit": limit}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# ── fundamentals & macro ────────────────────────────────────────────
|
|
255
|
+
def get_financials(
|
|
256
|
+
self,
|
|
257
|
+
ticker: str,
|
|
258
|
+
*,
|
|
259
|
+
statement: str = "income",
|
|
260
|
+
period: str = "annual",
|
|
261
|
+
limit: Optional[int] = None,
|
|
262
|
+
) -> dict:
|
|
263
|
+
"""Financial statements: 'income', 'balance-sheet' or 'cash-flow'."""
|
|
264
|
+
return self._get(
|
|
265
|
+
f"/financials/{ticker.upper()}",
|
|
266
|
+
{"statement": statement, "period": period, "limit": limit},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def get_revenue_segmentation(
|
|
270
|
+
self,
|
|
271
|
+
ticker: str,
|
|
272
|
+
*,
|
|
273
|
+
dimension: str = "product",
|
|
274
|
+
period: str = "annual",
|
|
275
|
+
) -> dict:
|
|
276
|
+
"""Revenue by product line or geography for equity tickers."""
|
|
277
|
+
return self._get(
|
|
278
|
+
f"/revenue-segmentation/{ticker.upper()}",
|
|
279
|
+
{"dimension": dimension, "period": period},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def get_allocation_breakdown(self, ticker: str) -> dict:
|
|
283
|
+
"""Sector and country weights for an ETF or mutual fund."""
|
|
284
|
+
return self._get(f"/allocation-breakdown/{ticker.upper()}")
|
|
285
|
+
|
|
286
|
+
def get_macro(self, *, category: Optional[str] = None, limit: Optional[int] = None) -> dict:
|
|
287
|
+
"""Macro series. Omit ``category`` to list available categories."""
|
|
288
|
+
return self._get("/macro", {"category": category, "limit": limit})
|
|
289
|
+
|
|
290
|
+
# ── filings ─────────────────────────────────────────────────────────
|
|
291
|
+
def get_filings(
|
|
292
|
+
self,
|
|
293
|
+
ticker: str,
|
|
294
|
+
*,
|
|
295
|
+
form: Optional[str] = None,
|
|
296
|
+
page: Optional[int] = None,
|
|
297
|
+
limit: Optional[int] = None,
|
|
298
|
+
) -> dict:
|
|
299
|
+
return self._get(
|
|
300
|
+
f"/filings/{ticker.upper()}", {"form": form, "page": page, "limit": limit}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def get_filing(self, filing_id: str) -> dict:
|
|
304
|
+
return self._get(f"/filings/by-id/{filing_id}")
|
|
305
|
+
|
|
306
|
+
# ── news ────────────────────────────────────────────────────────────
|
|
307
|
+
def get_news(
|
|
308
|
+
self,
|
|
309
|
+
*,
|
|
310
|
+
type: Optional[str] = None,
|
|
311
|
+
page: Optional[int] = None,
|
|
312
|
+
limit: Optional[int] = None,
|
|
313
|
+
) -> dict:
|
|
314
|
+
return self._get("/news", {"type": type, "page": page, "limit": limit})
|
|
315
|
+
|
|
316
|
+
def get_ticker_news(
|
|
317
|
+
self,
|
|
318
|
+
ticker: str,
|
|
319
|
+
*,
|
|
320
|
+
type: Optional[str] = None,
|
|
321
|
+
page: Optional[int] = None,
|
|
322
|
+
limit: Optional[int] = None,
|
|
323
|
+
) -> dict:
|
|
324
|
+
return self._get(
|
|
325
|
+
f"/news/{ticker.upper()}", {"type": type, "page": page, "limit": limit}
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# ── transcripts ─────────────────────────────────────────────────────
|
|
329
|
+
def list_transcript_tickers(
|
|
330
|
+
self,
|
|
331
|
+
*,
|
|
332
|
+
search: Optional[str] = None,
|
|
333
|
+
cursor: Optional[str] = None,
|
|
334
|
+
limit: Optional[int] = None,
|
|
335
|
+
) -> dict:
|
|
336
|
+
return self._get(
|
|
337
|
+
"/transcripts/tickers",
|
|
338
|
+
{"search": search, "cursor": cursor, "limit": limit},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def list_transcripts(
|
|
342
|
+
self, ticker: str, *, page: Optional[int] = None, limit: Optional[int] = None
|
|
343
|
+
) -> dict:
|
|
344
|
+
return self._get(f"/transcripts/{ticker.upper()}", {"page": page, "limit": limit})
|
|
345
|
+
|
|
346
|
+
def get_transcript(self, ticker: str, transcript_id: str) -> dict:
|
|
347
|
+
return self._get(f"/transcripts/{ticker.upper()}/{transcript_id}")
|
|
348
|
+
|
|
349
|
+
# ── earnings materials ──────────────────────────────────────────────
|
|
350
|
+
def list_earnings_materials(
|
|
351
|
+
self,
|
|
352
|
+
ticker: str,
|
|
353
|
+
*,
|
|
354
|
+
period: Optional[str] = None,
|
|
355
|
+
fiscal_year: Optional[int] = None,
|
|
356
|
+
page: Optional[int] = None,
|
|
357
|
+
limit: Optional[int] = None,
|
|
358
|
+
) -> dict:
|
|
359
|
+
"""List IR decks, press releases, and supplements for a ticker."""
|
|
360
|
+
return self._get(
|
|
361
|
+
f"/earnings-materials/{ticker.upper()}",
|
|
362
|
+
{"period": period, "fiscal_year": fiscal_year, "page": page, "limit": limit},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def get_earnings_material(self, ticker: str, material_id: str) -> dict:
|
|
366
|
+
"""Metadata for a single earnings material."""
|
|
367
|
+
return self._get(f"/earnings-materials/{ticker.upper()}/{material_id}")
|
|
368
|
+
|
|
369
|
+
def close(self) -> None:
|
|
370
|
+
self._session.close()
|
|
371
|
+
|
|
372
|
+
def __enter__(self) -> "RESTClient":
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
def __exit__(self, *exc) -> None:
|
|
376
|
+
self.close()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Exception hierarchy for the Vantafin client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VantafinError(Exception):
|
|
9
|
+
"""Base error for all Vantafin API failures."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, *, status_code: Optional[int] = None, response=None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.response = response
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthenticationError(VantafinError):
|
|
18
|
+
"""Raised on 401 - missing, invalid or revoked API key."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BadRequestError(VantafinError):
|
|
22
|
+
"""Raised on 400 - invalid parameters."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotFoundError(VantafinError):
|
|
26
|
+
"""Raised on 404 - resource does not exist."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RateLimitError(VantafinError):
|
|
30
|
+
"""Raised on 429 - too many requests. ``retry_after`` is seconds to wait."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, *, retry_after: Optional[int] = None, **kwargs):
|
|
33
|
+
super().__init__(message, **kwargs)
|
|
34
|
+
self.retry_after = retry_after
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ServerError(VantafinError):
|
|
38
|
+
"""Raised on 5xx - server-side failure."""
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""WebSocket client for the Vantafin real-time Data API.
|
|
2
|
+
|
|
3
|
+
Streams live news / press releases (``N.<ticker>`` or ``N.*``),
|
|
4
|
+
live SEC filings (``F.<ticker>`` or ``F.*``), and trading halts
|
|
5
|
+
(``H.<ticker>`` or ``H.*``) over a single connection to
|
|
6
|
+
``wss://socket.vantafin.com/v1/stocks``.
|
|
7
|
+
|
|
8
|
+
Requires the optional ``websocket-client`` dependency::
|
|
9
|
+
|
|
10
|
+
pip install vantafin[websocket]
|
|
11
|
+
|
|
12
|
+
Example::
|
|
13
|
+
|
|
14
|
+
from vantafin import WebSocketClient
|
|
15
|
+
|
|
16
|
+
ws = WebSocketClient("vf-live-your_api_key")
|
|
17
|
+
ws.connect()
|
|
18
|
+
ws.subscribe(["N.AAPL", "F.AAPL", "H.AAPL"])
|
|
19
|
+
for message in ws:
|
|
20
|
+
print(message)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from typing import Iterable, Iterator, Optional, Union
|
|
27
|
+
|
|
28
|
+
DEFAULT_WS_URL = "wss://socket.vantafin.com/v1/stocks"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WebSocketClient:
|
|
32
|
+
"""Synchronous WebSocket client for Vantafin real-time streams.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
api_key: Your Vantafin API key (``vf-live-...``).
|
|
36
|
+
url: Override the WebSocket URL (defaults to production).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key: str, *, url: str = DEFAULT_WS_URL) -> None:
|
|
40
|
+
if not api_key or not isinstance(api_key, str):
|
|
41
|
+
raise ValueError("api_key is required (e.g. 'vf-live-...').")
|
|
42
|
+
self.api_key = api_key.strip()
|
|
43
|
+
self.url = url
|
|
44
|
+
self._ws = None
|
|
45
|
+
|
|
46
|
+
def connect(self, *, timeout: Optional[float] = None) -> "WebSocketClient":
|
|
47
|
+
"""Open the connection, authenticating via the ``apiKey`` query parameter."""
|
|
48
|
+
try:
|
|
49
|
+
import websocket # type: ignore
|
|
50
|
+
except ImportError as exc: # pragma: no cover
|
|
51
|
+
raise ImportError(
|
|
52
|
+
"WebSocketClient requires the 'websocket-client' package. "
|
|
53
|
+
"Install it with: pip install vantafin[websocket]"
|
|
54
|
+
) from exc
|
|
55
|
+
|
|
56
|
+
sep = "&" if "?" in self.url else "?"
|
|
57
|
+
full_url = f"{self.url}{sep}apiKey={self.api_key}"
|
|
58
|
+
self._ws = websocket.create_connection(full_url, timeout=timeout)
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def subscribe(self, channels: Union[str, Iterable[str]]) -> None:
|
|
62
|
+
"""Subscribe to one or more channels, e.g. ``"N.AAPL"`` or ``["N.*", "F.AAPL", "H.*"]``."""
|
|
63
|
+
self._send("subscribe", channels)
|
|
64
|
+
|
|
65
|
+
def unsubscribe(self, channels: Union[str, Iterable[str]]) -> None:
|
|
66
|
+
"""Unsubscribe from one or more channels."""
|
|
67
|
+
self._send("unsubscribe", channels)
|
|
68
|
+
|
|
69
|
+
def _send(self, action: str, channels: Union[str, Iterable[str]]) -> None:
|
|
70
|
+
if self._ws is None:
|
|
71
|
+
raise RuntimeError("WebSocket is not connected; call connect() first.")
|
|
72
|
+
if isinstance(channels, str):
|
|
73
|
+
channels = [channels]
|
|
74
|
+
params = ",".join(c.strip().upper() for c in channels)
|
|
75
|
+
self._ws.send(json.dumps({"action": action, "params": params}))
|
|
76
|
+
|
|
77
|
+
def recv(self) -> dict:
|
|
78
|
+
"""Block for the next message and return it parsed as a dict."""
|
|
79
|
+
if self._ws is None:
|
|
80
|
+
raise RuntimeError("WebSocket is not connected; call connect() first.")
|
|
81
|
+
raw = self._ws.recv()
|
|
82
|
+
try:
|
|
83
|
+
return json.loads(raw)
|
|
84
|
+
except (json.JSONDecodeError, TypeError):
|
|
85
|
+
return {"type": "raw", "data": raw}
|
|
86
|
+
|
|
87
|
+
def __iter__(self) -> Iterator[dict]:
|
|
88
|
+
while True:
|
|
89
|
+
try:
|
|
90
|
+
yield self.recv()
|
|
91
|
+
except Exception:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
def close(self) -> None:
|
|
95
|
+
if self._ws is not None:
|
|
96
|
+
try:
|
|
97
|
+
self._ws.close()
|
|
98
|
+
finally:
|
|
99
|
+
self._ws = None
|
|
100
|
+
|
|
101
|
+
def __enter__(self) -> "WebSocketClient":
|
|
102
|
+
return self.connect()
|
|
103
|
+
|
|
104
|
+
def __exit__(self, *exc) -> None:
|
|
105
|
+
self.close()
|