fathom-charts 0.2.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.
- fathom_charts-0.2.0/.gitignore +6 -0
- fathom_charts-0.2.0/LICENSE +38 -0
- fathom_charts-0.2.0/PKG-INFO +161 -0
- fathom_charts-0.2.0/README.md +133 -0
- fathom_charts-0.2.0/pyproject.toml +68 -0
- fathom_charts-0.2.0/src/fathom_charts/__init__.py +154 -0
- fathom_charts-0.2.0/src/fathom_charts/errors.py +91 -0
- fathom_charts-0.2.0/src/fathom_charts/generate_types.py +237 -0
- fathom_charts-0.2.0/src/fathom_charts/protocol.py +216 -0
- fathom_charts-0.2.0/src/fathom_charts/py.typed +0 -0
- fathom_charts-0.2.0/src/fathom_charts/rest.py +664 -0
- fathom_charts-0.2.0/src/fathom_charts/stream.py +804 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Fathom Charts SDK License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrien Insights. All rights reserved.
|
|
4
|
+
|
|
5
|
+
The fathom-charts Python package (the "Software") is proprietary. It is licensed,
|
|
6
|
+
not sold.
|
|
7
|
+
|
|
8
|
+
1. Grant. Subject to these terms, Adrien Insights grants you a non-exclusive,
|
|
9
|
+
non-transferable, revocable, royalty-free license to install, use and copy
|
|
10
|
+
the Software, and to include it, in source or compiled, bundled or minified
|
|
11
|
+
form, in your own applications, solely to access the Fathom Charts service
|
|
12
|
+
(fathomcharts.com, its REST API and its WebSocket stream, the "Service")
|
|
13
|
+
under the Service's terms of use.
|
|
14
|
+
|
|
15
|
+
2. Restrictions. You may not: (a) use the Software, or any part or derivative of
|
|
16
|
+
it, to access or build a client for any service other than the Service;
|
|
17
|
+
(b) distribute, sell, sublicense or publish the Software on its own, as a
|
|
18
|
+
package, library or SDK, other than as part of your own application;
|
|
19
|
+
(c) remove or alter this notice.
|
|
20
|
+
|
|
21
|
+
3. Ownership. The Software and all rights in it remain the property of
|
|
22
|
+
Adrien Insights. No right is granted other than those expressly stated
|
|
23
|
+
above. Using the Service also requires an account and an API key, governed
|
|
24
|
+
by the Service's terms of use and terms of sale.
|
|
25
|
+
|
|
26
|
+
4. Termination. This license ends automatically if you breach these terms or
|
|
27
|
+
when your right to use the Service ends. You must then stop using the
|
|
28
|
+
Software and remove it from your applications.
|
|
29
|
+
|
|
30
|
+
5. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
31
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
32
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. TO
|
|
33
|
+
THE EXTENT PERMITTED BY LAW, ADRIEN INSIGHTS SHALL NOT BE LIABLE FOR ANY
|
|
34
|
+
CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE SOFTWARE OR ITS USE.
|
|
35
|
+
|
|
36
|
+
6. Law. These terms are governed by French law.
|
|
37
|
+
|
|
38
|
+
Contact: support@fathomcharts.com
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fathom-charts
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Fathom Charts client: real-time order-flow indicator streams (WebSocket) and history (REST), for Python 3.11+ (asyncio).
|
|
5
|
+
Project-URL: Homepage, https://fathomcharts.com/docs/sdk/python
|
|
6
|
+
Project-URL: Documentation, https://fathomcharts.com/docs/sdk/python
|
|
7
|
+
Author-email: Adrien Insights <support@fathomcharts.com>
|
|
8
|
+
License-Expression: LicenseRef-Proprietary
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: fathom-charts,footprint,indicators,market-data,order-flow,trading,websocket
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.28
|
|
25
|
+
Requires-Dist: websockets>=15
|
|
26
|
+
Requires-Dist: zstandard>=0.23
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# fathom-charts
|
|
30
|
+
|
|
31
|
+
Python client for [Fathom Charts](https://fathomcharts.com): real-time order-flow indicator streams (WebSocket) and history (REST), for Python 3.11+ (asyncio).
|
|
32
|
+
|
|
33
|
+
The SDK handles authentication, the initial snapshot, resuming after a disconnect, duplicates, history pagination and rate-limit retries.
|
|
34
|
+
|
|
35
|
+
Documentation: [fathomcharts.com/docs/sdk/python](https://fathomcharts.com/docs/sdk/python)
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install fathom-charts
|
|
41
|
+
# or
|
|
42
|
+
uv add fathom-charts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Generate the types of each indicator's parameters and data from the catalog (`GET /v1/indicators`, no authentication):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
curl https://api.fathomcharts.com/v1/indicators -o catalog.json
|
|
49
|
+
fathom-charts-types catalog.json fathom_charts_types.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The command also takes the catalog URL instead of a file. Each indicator gets `<Name>Params` and `<Name>Data` (`TypedDict`), named after its id (`big-trades` → `BigTradesParams`), with the catalog's descriptions as docstrings; `IndicatorParams`, `IndicatorData` and `IndicatorId` cover them all. A tuple whose last elements are optional is the union of its allowed lengths (`tuple[int, int, int] | tuple[int, int, int, int]`); JSON arrays still arrive as lists.
|
|
53
|
+
|
|
54
|
+
## Real time
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import asyncio
|
|
58
|
+
import os
|
|
59
|
+
|
|
60
|
+
from fathom_charts import FathomChartsStream, Subscription, Upsert
|
|
61
|
+
from fathom_charts_types import BigTradesData
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def main() -> None:
|
|
65
|
+
async with FathomChartsStream(url="wss://stream.fathomcharts.com/v1", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as stream:
|
|
66
|
+
big_trades: Subscription[BigTradesData]
|
|
67
|
+
async with stream.subscribe(
|
|
68
|
+
instrument="NQ.front",
|
|
69
|
+
indicator="big-trades",
|
|
70
|
+
params={"minimum": 30},
|
|
71
|
+
mode="confirmed",
|
|
72
|
+
) as big_trades:
|
|
73
|
+
async for event in big_trades:
|
|
74
|
+
if isinstance(event, Upsert):
|
|
75
|
+
print(event.data["side"], event.data["volume"], event.data["price"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The events are frozen dataclasses: `Subscribed`, `Snapshot` (its `items` are `Item`), `Upsert`, `Remove`, `Status`, `Reset` and `Notice`, each with its `type` (`'upsert'`…), to test with `isinstance` or `match`. Heartbeats and pongs are handled by the SDK; notices (a restart, a maintenance window announced or cancelled) reach every subscription and `on_notice`; an `error` on the subscription is raised by the loop as a `FathomChartsError`. Leaving the `async with` block of a subscription unsubscribes (leaving an `async for` loop alone does not, in Python: use `async with`, or `await sub.close()`); the connection closes when no subscription is left. See [Real time](https://fathomcharts.com/docs/websocket).
|
|
82
|
+
|
|
83
|
+
An application distributed to end users must not hold the API key: give `stream_token` instead of `api_key`, an async function returning a fresh single-use token that your backend gets from `POST /v1/stream-tokens` (`FathomChartsRest.stream_token()`), called for every connection.
|
|
84
|
+
|
|
85
|
+
## History
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from fathom_charts import FathomChartsRest
|
|
89
|
+
|
|
90
|
+
async with FathomChartsRest(base_url="https://api.fathomcharts.com", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as rest:
|
|
91
|
+
async for m in rest.history({
|
|
92
|
+
"instrument": "NQZ6",
|
|
93
|
+
"indicator": "big-trades",
|
|
94
|
+
"params": {"minimum": 30},
|
|
95
|
+
"from": "1790256600000000000",
|
|
96
|
+
"to": "1790258100000000000",
|
|
97
|
+
}):
|
|
98
|
+
if m["t"] == "upsert" and m["final"] and "data" in m:
|
|
99
|
+
print(m["cursor"], m["data"]["side"], m["data"]["volume"])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Queries are dictionaries with the fields of the API (`instrument`, `indicator`, `params`, `from`, `to`, `mode`, `limit`, `cursor`, `untilCursor`, `snapshot`); with `"snapshot": True`, `history()` yields the state at `from` first, as a `{"t": "snapshot", "cursor": …, "items": […]}` element. `page()` and `pages()` give the pages themselves, `estimate()` the cost of a query without running it. On error, the SDK raises a `FathomChartsError` carrying the error `code`. See [History](https://fathomcharts.com/docs/history).
|
|
103
|
+
|
|
104
|
+
## Export to a file
|
|
105
|
+
|
|
106
|
+
`export_ndjson()` decompresses an export as NDJSON (one mutation per line), in chunks of whole lines, and resumes it by itself after a cut, without gap or duplicate:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from contextlib import aclosing
|
|
110
|
+
|
|
111
|
+
from fathom_charts import RangeQuery
|
|
112
|
+
|
|
113
|
+
query: RangeQuery = {"instrument": "NQZ6", "indicator": "big-trades", "params": {"minimum": 30}, "from": "1790256600000000000", "to": "1790258100000000000"}
|
|
114
|
+
async with FathomChartsRest(base_url="https://api.fathomcharts.com", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as rest:
|
|
115
|
+
with open("big-trades.ndjson", "wb") as file:
|
|
116
|
+
async with aclosing(rest.export_ndjson(query)) as chunks:
|
|
117
|
+
async for chunk in chunks:
|
|
118
|
+
file.write(chunk)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`max_resumes` (default 5) bounds the consecutive resumptions without a new line, `resume_delay` (1 s, doubled up to 30 s) spaces them. `export()` gives the zstd-compressed file as sent (`body`, `content_type`, `filename`, and the `token` that resumes it).
|
|
122
|
+
|
|
123
|
+
## Connection, resume and flow control
|
|
124
|
+
|
|
125
|
+
`FathomChartsStream` multiplexes its subscriptions on one connection and reopens it after a cut (backoff with jitter), resuming each subscription after its last cursor received, with the engine build its state comes from; events already received are never delivered twice. During an update of the service the stream pauses and resumes by itself; when the computation of an indicator changed, the subscription receives `reset` (`engine_change`), `subscribed` and a new snapshot. See [Updates and maintenance](https://fathomcharts.com/docs/maintenance).
|
|
126
|
+
|
|
127
|
+
Durations are in seconds.
|
|
128
|
+
|
|
129
|
+
- A resume within the server's memory of recent messages continues where it stopped. An older one (a long cut, or a long `from_={"time": …}` replay interrupted) is caught up from history when your offer includes it and its depth reaches the cursor's UTC day: the rest of the replay follows, then real time, without `reset` nor snapshot (the historical part is charged like the `from_={"time": …}`). Otherwise the subscription receives `reset` (`cursor_expired`) followed by a fresh snapshot.
|
|
130
|
+
- `high_water_mark` (default 10 000 queued events): above it, the SDK stops reading the socket (TCP backpressure up to the server) and queued provisional upserts of the same object are merged. While paused, the SDK sends `ping` messages so that the server keeps the connection.
|
|
131
|
+
- `max_buffered` (default 4 × `high_water_mark`): a socket given by a `web_socket` factory that cannot be paused (`pausable` false) stops being read there: the SDK closes the connection (close code 4000, `client buffer full`) and reopens it from the last queued cursor once the consumer has read half the high-water mark. Nothing queued is lost, and the resume rules above apply.
|
|
132
|
+
- `heartbeat_timeout` (default 15 s): without any message for this long (the server sends `hb` every 5 s), the connection is dropped and reopened.
|
|
133
|
+
- `reconnect=Reconnect(...)`: `initial_delay` (0.25 s), `max_delay` (30 s), `quota_retry` (how long close code 4029 is retried, 120 s), `quota_delay` (5 s).
|
|
134
|
+
- `random`: source of the reconnection jitter, in [0, 1) (default `random.random`; for tests).
|
|
135
|
+
- `on_notice` receives the server notices (`reconnect`; `maintenance` with `id`, `effective_at`, `ends_at`, `message`; `maintenance-cancelled` with `id`; `breaking-change` with `effective_at`), also given to every subscription as `Notice` events; `stream.maintenance` lists the maintenance windows announced and not over; `on_state_change` the connection state (`idle`, `connecting`, `open`, `reconnecting`, `closed`), also readable as `stream.connection_state`.
|
|
136
|
+
|
|
137
|
+
A `Subscription` is an async iterator of events, with:
|
|
138
|
+
|
|
139
|
+
- `id`: its `sub` on the connection (`s1`, `s2`…), as in the protocol messages;
|
|
140
|
+
- `request`: the subscription as requested, defaults filled in (`params={}`, `mode='live'`, `from_='live'`);
|
|
141
|
+
- `cursor`: cursor of the last event handed to your loop, the resume point; a heartbeat also advances it while every received event is consumed, so it moves even when nothing is published;
|
|
142
|
+
- `topic`: topic key from the latest `subscribed`;
|
|
143
|
+
- `engine`: engine build from the latest `subscribed`; save it with `cursor` and resume a later process with `from_={"cursor": cursor, "engine": engine}`;
|
|
144
|
+
- `close()`: unsubscribes (as leaving its `async with` block does).
|
|
145
|
+
|
|
146
|
+
`CloseCode` lists the gateway's WebSocket close codes; `compare_cursors(a, b)` orders two cursors (negative, zero, positive).
|
|
147
|
+
|
|
148
|
+
## Errors
|
|
149
|
+
|
|
150
|
+
Every error the SDK raises, or ends an iteration with, is a `FathomChartsError` (`ERROR_CODES` lists the codes):
|
|
151
|
+
|
|
152
|
+
- `code`: the API's code (`INVALID_PARAMETERS`, `FORBIDDEN`, `QUOTA_EXCEEDED`…), or one of the SDK's own: `CONFIGURATION` (invalid options: missing or empty `api_key`, both `api_key` and `stream_token`…), `CLOSED`, `KEY_REVOKED`, `ENTITLEMENTS_CHANGED`, `TOO_MANY_CONNECTIONS`, `PROTOCOL_ERROR`, `BINARY_NOT_SUPPORTED`, `INVALID_UTF8`, `MESSAGE_TOO_BIG`, `NETWORK_ERROR` (the API could not be reached, or the connection was lost during an answer), or `HTTP_<status>` for an HTTP error answer that is not a problem document;
|
|
153
|
+
- `status`, `close_code`, `retry_after` when known;
|
|
154
|
+
- `problem`: the RFC 9457 problem document of an HTTP error, with its `errors` (invalid fields); `None` when the answer is not a problem document;
|
|
155
|
+
- `__cause__`: the original error (the error of httpx…), when there is one.
|
|
156
|
+
|
|
157
|
+
A call is stopped by cancelling its task (`asyncio.timeout()`, `Task.cancel()`), a wait for a retry included: the `asyncio.CancelledError` goes through untouched, as asyncio requires.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
Proprietary: you may use this package and include it in your own applications only to access the Fathom Charts service. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# fathom-charts
|
|
2
|
+
|
|
3
|
+
Python client for [Fathom Charts](https://fathomcharts.com): real-time order-flow indicator streams (WebSocket) and history (REST), for Python 3.11+ (asyncio).
|
|
4
|
+
|
|
5
|
+
The SDK handles authentication, the initial snapshot, resuming after a disconnect, duplicates, history pagination and rate-limit retries.
|
|
6
|
+
|
|
7
|
+
Documentation: [fathomcharts.com/docs/sdk/python](https://fathomcharts.com/docs/sdk/python)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fathom-charts
|
|
13
|
+
# or
|
|
14
|
+
uv add fathom-charts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Generate the types of each indicator's parameters and data from the catalog (`GET /v1/indicators`, no authentication):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
curl https://api.fathomcharts.com/v1/indicators -o catalog.json
|
|
21
|
+
fathom-charts-types catalog.json fathom_charts_types.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The command also takes the catalog URL instead of a file. Each indicator gets `<Name>Params` and `<Name>Data` (`TypedDict`), named after its id (`big-trades` → `BigTradesParams`), with the catalog's descriptions as docstrings; `IndicatorParams`, `IndicatorData` and `IndicatorId` cover them all. A tuple whose last elements are optional is the union of its allowed lengths (`tuple[int, int, int] | tuple[int, int, int, int]`); JSON arrays still arrive as lists.
|
|
25
|
+
|
|
26
|
+
## Real time
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import asyncio
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
from fathom_charts import FathomChartsStream, Subscription, Upsert
|
|
33
|
+
from fathom_charts_types import BigTradesData
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main() -> None:
|
|
37
|
+
async with FathomChartsStream(url="wss://stream.fathomcharts.com/v1", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as stream:
|
|
38
|
+
big_trades: Subscription[BigTradesData]
|
|
39
|
+
async with stream.subscribe(
|
|
40
|
+
instrument="NQ.front",
|
|
41
|
+
indicator="big-trades",
|
|
42
|
+
params={"minimum": 30},
|
|
43
|
+
mode="confirmed",
|
|
44
|
+
) as big_trades:
|
|
45
|
+
async for event in big_trades:
|
|
46
|
+
if isinstance(event, Upsert):
|
|
47
|
+
print(event.data["side"], event.data["volume"], event.data["price"])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The events are frozen dataclasses: `Subscribed`, `Snapshot` (its `items` are `Item`), `Upsert`, `Remove`, `Status`, `Reset` and `Notice`, each with its `type` (`'upsert'`…), to test with `isinstance` or `match`. Heartbeats and pongs are handled by the SDK; notices (a restart, a maintenance window announced or cancelled) reach every subscription and `on_notice`; an `error` on the subscription is raised by the loop as a `FathomChartsError`. Leaving the `async with` block of a subscription unsubscribes (leaving an `async for` loop alone does not, in Python: use `async with`, or `await sub.close()`); the connection closes when no subscription is left. See [Real time](https://fathomcharts.com/docs/websocket).
|
|
54
|
+
|
|
55
|
+
An application distributed to end users must not hold the API key: give `stream_token` instead of `api_key`, an async function returning a fresh single-use token that your backend gets from `POST /v1/stream-tokens` (`FathomChartsRest.stream_token()`), called for every connection.
|
|
56
|
+
|
|
57
|
+
## History
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from fathom_charts import FathomChartsRest
|
|
61
|
+
|
|
62
|
+
async with FathomChartsRest(base_url="https://api.fathomcharts.com", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as rest:
|
|
63
|
+
async for m in rest.history({
|
|
64
|
+
"instrument": "NQZ6",
|
|
65
|
+
"indicator": "big-trades",
|
|
66
|
+
"params": {"minimum": 30},
|
|
67
|
+
"from": "1790256600000000000",
|
|
68
|
+
"to": "1790258100000000000",
|
|
69
|
+
}):
|
|
70
|
+
if m["t"] == "upsert" and m["final"] and "data" in m:
|
|
71
|
+
print(m["cursor"], m["data"]["side"], m["data"]["volume"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Queries are dictionaries with the fields of the API (`instrument`, `indicator`, `params`, `from`, `to`, `mode`, `limit`, `cursor`, `untilCursor`, `snapshot`); with `"snapshot": True`, `history()` yields the state at `from` first, as a `{"t": "snapshot", "cursor": …, "items": […]}` element. `page()` and `pages()` give the pages themselves, `estimate()` the cost of a query without running it. On error, the SDK raises a `FathomChartsError` carrying the error `code`. See [History](https://fathomcharts.com/docs/history).
|
|
75
|
+
|
|
76
|
+
## Export to a file
|
|
77
|
+
|
|
78
|
+
`export_ndjson()` decompresses an export as NDJSON (one mutation per line), in chunks of whole lines, and resumes it by itself after a cut, without gap or duplicate:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from contextlib import aclosing
|
|
82
|
+
|
|
83
|
+
from fathom_charts import RangeQuery
|
|
84
|
+
|
|
85
|
+
query: RangeQuery = {"instrument": "NQZ6", "indicator": "big-trades", "params": {"minimum": 30}, "from": "1790256600000000000", "to": "1790258100000000000"}
|
|
86
|
+
async with FathomChartsRest(base_url="https://api.fathomcharts.com", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as rest:
|
|
87
|
+
with open("big-trades.ndjson", "wb") as file:
|
|
88
|
+
async with aclosing(rest.export_ndjson(query)) as chunks:
|
|
89
|
+
async for chunk in chunks:
|
|
90
|
+
file.write(chunk)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`max_resumes` (default 5) bounds the consecutive resumptions without a new line, `resume_delay` (1 s, doubled up to 30 s) spaces them. `export()` gives the zstd-compressed file as sent (`body`, `content_type`, `filename`, and the `token` that resumes it).
|
|
94
|
+
|
|
95
|
+
## Connection, resume and flow control
|
|
96
|
+
|
|
97
|
+
`FathomChartsStream` multiplexes its subscriptions on one connection and reopens it after a cut (backoff with jitter), resuming each subscription after its last cursor received, with the engine build its state comes from; events already received are never delivered twice. During an update of the service the stream pauses and resumes by itself; when the computation of an indicator changed, the subscription receives `reset` (`engine_change`), `subscribed` and a new snapshot. See [Updates and maintenance](https://fathomcharts.com/docs/maintenance).
|
|
98
|
+
|
|
99
|
+
Durations are in seconds.
|
|
100
|
+
|
|
101
|
+
- A resume within the server's memory of recent messages continues where it stopped. An older one (a long cut, or a long `from_={"time": …}` replay interrupted) is caught up from history when your offer includes it and its depth reaches the cursor's UTC day: the rest of the replay follows, then real time, without `reset` nor snapshot (the historical part is charged like the `from_={"time": …}`). Otherwise the subscription receives `reset` (`cursor_expired`) followed by a fresh snapshot.
|
|
102
|
+
- `high_water_mark` (default 10 000 queued events): above it, the SDK stops reading the socket (TCP backpressure up to the server) and queued provisional upserts of the same object are merged. While paused, the SDK sends `ping` messages so that the server keeps the connection.
|
|
103
|
+
- `max_buffered` (default 4 × `high_water_mark`): a socket given by a `web_socket` factory that cannot be paused (`pausable` false) stops being read there: the SDK closes the connection (close code 4000, `client buffer full`) and reopens it from the last queued cursor once the consumer has read half the high-water mark. Nothing queued is lost, and the resume rules above apply.
|
|
104
|
+
- `heartbeat_timeout` (default 15 s): without any message for this long (the server sends `hb` every 5 s), the connection is dropped and reopened.
|
|
105
|
+
- `reconnect=Reconnect(...)`: `initial_delay` (0.25 s), `max_delay` (30 s), `quota_retry` (how long close code 4029 is retried, 120 s), `quota_delay` (5 s).
|
|
106
|
+
- `random`: source of the reconnection jitter, in [0, 1) (default `random.random`; for tests).
|
|
107
|
+
- `on_notice` receives the server notices (`reconnect`; `maintenance` with `id`, `effective_at`, `ends_at`, `message`; `maintenance-cancelled` with `id`; `breaking-change` with `effective_at`), also given to every subscription as `Notice` events; `stream.maintenance` lists the maintenance windows announced and not over; `on_state_change` the connection state (`idle`, `connecting`, `open`, `reconnecting`, `closed`), also readable as `stream.connection_state`.
|
|
108
|
+
|
|
109
|
+
A `Subscription` is an async iterator of events, with:
|
|
110
|
+
|
|
111
|
+
- `id`: its `sub` on the connection (`s1`, `s2`…), as in the protocol messages;
|
|
112
|
+
- `request`: the subscription as requested, defaults filled in (`params={}`, `mode='live'`, `from_='live'`);
|
|
113
|
+
- `cursor`: cursor of the last event handed to your loop, the resume point; a heartbeat also advances it while every received event is consumed, so it moves even when nothing is published;
|
|
114
|
+
- `topic`: topic key from the latest `subscribed`;
|
|
115
|
+
- `engine`: engine build from the latest `subscribed`; save it with `cursor` and resume a later process with `from_={"cursor": cursor, "engine": engine}`;
|
|
116
|
+
- `close()`: unsubscribes (as leaving its `async with` block does).
|
|
117
|
+
|
|
118
|
+
`CloseCode` lists the gateway's WebSocket close codes; `compare_cursors(a, b)` orders two cursors (negative, zero, positive).
|
|
119
|
+
|
|
120
|
+
## Errors
|
|
121
|
+
|
|
122
|
+
Every error the SDK raises, or ends an iteration with, is a `FathomChartsError` (`ERROR_CODES` lists the codes):
|
|
123
|
+
|
|
124
|
+
- `code`: the API's code (`INVALID_PARAMETERS`, `FORBIDDEN`, `QUOTA_EXCEEDED`…), or one of the SDK's own: `CONFIGURATION` (invalid options: missing or empty `api_key`, both `api_key` and `stream_token`…), `CLOSED`, `KEY_REVOKED`, `ENTITLEMENTS_CHANGED`, `TOO_MANY_CONNECTIONS`, `PROTOCOL_ERROR`, `BINARY_NOT_SUPPORTED`, `INVALID_UTF8`, `MESSAGE_TOO_BIG`, `NETWORK_ERROR` (the API could not be reached, or the connection was lost during an answer), or `HTTP_<status>` for an HTTP error answer that is not a problem document;
|
|
125
|
+
- `status`, `close_code`, `retry_after` when known;
|
|
126
|
+
- `problem`: the RFC 9457 problem document of an HTTP error, with its `errors` (invalid fields); `None` when the answer is not a problem document;
|
|
127
|
+
- `__cause__`: the original error (the error of httpx…), when there is one.
|
|
128
|
+
|
|
129
|
+
A call is stopped by cancelling its task (`asyncio.timeout()`, `Task.cancel()`), a wait for a retry included: the `asyncio.CancelledError` goes through untouched, as asyncio requires.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Proprietary: you may use this package and include it in your own applications only to access the Fathom Charts service. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fathom-charts"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Fathom Charts client: real-time order-flow indicator streams (WebSocket) and history (REST), for Python 3.11+ (asyncio)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "LicenseRef-Proprietary"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Adrien Insights", email = "support@fathomcharts.com" }]
|
|
10
|
+
keywords = ["fathom-charts", "order-flow", "footprint", "indicators", "market-data", "websocket", "trading"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Framework :: AsyncIO",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.28",
|
|
27
|
+
"websockets>=15",
|
|
28
|
+
"zstandard>=0.23",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://fathomcharts.com/docs/sdk/python"
|
|
33
|
+
Documentation = "https://fathomcharts.com/docs/sdk/python"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
fathom-charts-types = "fathom_charts.generate_types:main"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"mypy>=1.15",
|
|
41
|
+
"pytest>=8.3",
|
|
42
|
+
"pytest-asyncio>=0.25",
|
|
43
|
+
"ruff>=0.9",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling>=1.27"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/fathom_charts"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
include = ["src/fathom_charts", "README.md", "LICENSE", "pyproject.toml"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
strict = true
|
|
63
|
+
python_version = "3.11"
|
|
64
|
+
files = ["src", "tests"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 140
|
|
68
|
+
target-version = "py311"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Fathom Charts client: real-time order-flow indicator streams (WebSocket) and history (REST)."""
|
|
2
|
+
|
|
3
|
+
from .errors import ERROR_CODES, ErrorCode, FathomChartsError, ProblemFieldError
|
|
4
|
+
from .protocol import (
|
|
5
|
+
AuthMessage,
|
|
6
|
+
ClientMessage,
|
|
7
|
+
CloseCode,
|
|
8
|
+
CursorFrom,
|
|
9
|
+
ErrorMessage,
|
|
10
|
+
From,
|
|
11
|
+
HeartbeatMessage,
|
|
12
|
+
MaintenanceWindow,
|
|
13
|
+
Mode,
|
|
14
|
+
NoticeKind,
|
|
15
|
+
NoticeMessage,
|
|
16
|
+
PingMessage,
|
|
17
|
+
PongMessage,
|
|
18
|
+
RemoveMessage,
|
|
19
|
+
ResetMessage,
|
|
20
|
+
ResetReason,
|
|
21
|
+
ServerMessage,
|
|
22
|
+
SnapshotItem,
|
|
23
|
+
SnapshotMessage,
|
|
24
|
+
SourceState,
|
|
25
|
+
StatusMessage,
|
|
26
|
+
SubscribedMessage,
|
|
27
|
+
SubscribeMessage,
|
|
28
|
+
TimeFrom,
|
|
29
|
+
UnsubscribeMessage,
|
|
30
|
+
UpsertMessage,
|
|
31
|
+
compare_cursors,
|
|
32
|
+
)
|
|
33
|
+
from .rest import (
|
|
34
|
+
Catalog,
|
|
35
|
+
CatalogField,
|
|
36
|
+
CatalogIndicator,
|
|
37
|
+
CatalogObject,
|
|
38
|
+
CatalogParam,
|
|
39
|
+
Estimate,
|
|
40
|
+
ExportResume,
|
|
41
|
+
ExportStream,
|
|
42
|
+
FathomChartsRest,
|
|
43
|
+
HistorySnapshot,
|
|
44
|
+
Instrument,
|
|
45
|
+
Instruments,
|
|
46
|
+
Limits,
|
|
47
|
+
Mutation,
|
|
48
|
+
OfferCode,
|
|
49
|
+
Offers,
|
|
50
|
+
Page,
|
|
51
|
+
PageQuery,
|
|
52
|
+
PageSnapshot,
|
|
53
|
+
PlanCode,
|
|
54
|
+
RangeQuery,
|
|
55
|
+
Resolved,
|
|
56
|
+
ServiceStatus,
|
|
57
|
+
StatusMaintenance,
|
|
58
|
+
StreamToken,
|
|
59
|
+
Usage,
|
|
60
|
+
)
|
|
61
|
+
from .stream import (
|
|
62
|
+
ConnectionState,
|
|
63
|
+
FathomChartsStream,
|
|
64
|
+
Item,
|
|
65
|
+
Notice,
|
|
66
|
+
Reconnect,
|
|
67
|
+
Remove,
|
|
68
|
+
Reset,
|
|
69
|
+
Snapshot,
|
|
70
|
+
Status,
|
|
71
|
+
StreamEvent,
|
|
72
|
+
Subscribed,
|
|
73
|
+
SubscribeRequest,
|
|
74
|
+
Subscription,
|
|
75
|
+
Upsert,
|
|
76
|
+
WebSocketFactory,
|
|
77
|
+
WebSocketLike,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"ERROR_CODES",
|
|
82
|
+
"AuthMessage",
|
|
83
|
+
"Catalog",
|
|
84
|
+
"CatalogField",
|
|
85
|
+
"CatalogIndicator",
|
|
86
|
+
"CatalogObject",
|
|
87
|
+
"CatalogParam",
|
|
88
|
+
"ClientMessage",
|
|
89
|
+
"CloseCode",
|
|
90
|
+
"ConnectionState",
|
|
91
|
+
"CursorFrom",
|
|
92
|
+
"ErrorCode",
|
|
93
|
+
"ErrorMessage",
|
|
94
|
+
"Estimate",
|
|
95
|
+
"ExportResume",
|
|
96
|
+
"ExportStream",
|
|
97
|
+
"FathomChartsError",
|
|
98
|
+
"FathomChartsRest",
|
|
99
|
+
"FathomChartsStream",
|
|
100
|
+
"From",
|
|
101
|
+
"HeartbeatMessage",
|
|
102
|
+
"HistorySnapshot",
|
|
103
|
+
"Instrument",
|
|
104
|
+
"Instruments",
|
|
105
|
+
"Item",
|
|
106
|
+
"Limits",
|
|
107
|
+
"MaintenanceWindow",
|
|
108
|
+
"Mode",
|
|
109
|
+
"Mutation",
|
|
110
|
+
"Notice",
|
|
111
|
+
"NoticeKind",
|
|
112
|
+
"NoticeMessage",
|
|
113
|
+
"OfferCode",
|
|
114
|
+
"Offers",
|
|
115
|
+
"Page",
|
|
116
|
+
"PageQuery",
|
|
117
|
+
"PageSnapshot",
|
|
118
|
+
"PingMessage",
|
|
119
|
+
"PlanCode",
|
|
120
|
+
"PongMessage",
|
|
121
|
+
"ProblemFieldError",
|
|
122
|
+
"RangeQuery",
|
|
123
|
+
"Reconnect",
|
|
124
|
+
"Remove",
|
|
125
|
+
"RemoveMessage",
|
|
126
|
+
"Reset",
|
|
127
|
+
"ResetMessage",
|
|
128
|
+
"ResetReason",
|
|
129
|
+
"Resolved",
|
|
130
|
+
"ServerMessage",
|
|
131
|
+
"ServiceStatus",
|
|
132
|
+
"Snapshot",
|
|
133
|
+
"SnapshotItem",
|
|
134
|
+
"SnapshotMessage",
|
|
135
|
+
"SourceState",
|
|
136
|
+
"Status",
|
|
137
|
+
"StatusMaintenance",
|
|
138
|
+
"StatusMessage",
|
|
139
|
+
"StreamEvent",
|
|
140
|
+
"StreamToken",
|
|
141
|
+
"SubscribeMessage",
|
|
142
|
+
"SubscribeRequest",
|
|
143
|
+
"Subscribed",
|
|
144
|
+
"SubscribedMessage",
|
|
145
|
+
"Subscription",
|
|
146
|
+
"TimeFrom",
|
|
147
|
+
"UnsubscribeMessage",
|
|
148
|
+
"Upsert",
|
|
149
|
+
"UpsertMessage",
|
|
150
|
+
"Usage",
|
|
151
|
+
"WebSocketFactory",
|
|
152
|
+
"WebSocketLike",
|
|
153
|
+
"compare_cursors",
|
|
154
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Stable error codes and the error the SDK raises."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
ErrorCode = Literal[
|
|
8
|
+
"INVALID_PARAMETERS", "UNKNOWN_INDICATOR", "UNKNOWN_INSTRUMENT", "NOT_COVERED", "WARMING", "QUOTA_EXCEEDED",
|
|
9
|
+
"RATE_LIMITED", "CAPACITY", "CURSOR_EXPIRED", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "METHOD_NOT_ALLOWED", "CONFLICT",
|
|
10
|
+
"WITHDRAWAL_WAIVER_REQUIRED", "SOURCE_DOWN", "UPSTREAM_ERROR", "INTERNAL",
|
|
11
|
+
"CONFIGURATION", "CLOSED", "KEY_REVOKED", "ENTITLEMENTS_CHANGED", "TOO_MANY_CONNECTIONS", "PROTOCOL_ERROR", "BINARY_NOT_SUPPORTED",
|
|
12
|
+
"INVALID_UTF8", "MESSAGE_TOO_BIG", "ABORTED", "NETWORK_ERROR",
|
|
13
|
+
]
|
|
14
|
+
"""A code of `ERROR_CODES`. An HTTP error answer without a problem document has the code `HTTP_<status>` (e.g. `HTTP_502`)."""
|
|
15
|
+
|
|
16
|
+
ERROR_CODES: tuple[ErrorCode, ...] = (
|
|
17
|
+
"INVALID_PARAMETERS", "UNKNOWN_INDICATOR", "UNKNOWN_INSTRUMENT", "NOT_COVERED", "WARMING", "QUOTA_EXCEEDED",
|
|
18
|
+
"RATE_LIMITED", "CAPACITY", "CURSOR_EXPIRED", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "METHOD_NOT_ALLOWED", "CONFLICT",
|
|
19
|
+
"WITHDRAWAL_WAIVER_REQUIRED", "SOURCE_DOWN", "UPSTREAM_ERROR", "INTERNAL",
|
|
20
|
+
"CONFIGURATION", "CLOSED", "KEY_REVOKED", "ENTITLEMENTS_CHANGED", "TOO_MANY_CONNECTIONS", "PROTOCOL_ERROR", "BINARY_NOT_SUPPORTED",
|
|
21
|
+
"INVALID_UTF8", "MESSAGE_TOO_BIG", "ABORTED", "NETWORK_ERROR",
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
Stable error codes.
|
|
25
|
+
|
|
26
|
+
Codes of the API (RFC 9457 `code`) and of the WebSocket `error` messages: INVALID_PARAMETERS to INTERNAL. FORBIDDEN covers
|
|
27
|
+
the rights of the offers: historical data (queries, estimates, exports, `from:{time}`) needs the Historical offer, real time
|
|
28
|
+
the Live offer. WITHDRAWAL_WAIVER_REQUIRED: a checkout without the waiver of the right of withdrawal.
|
|
29
|
+
|
|
30
|
+
Codes raised by the SDK itself:
|
|
31
|
+
- CONFIGURATION: invalid client options.
|
|
32
|
+
- CLOSED: `subscribe()` on a stream closed by `close()`.
|
|
33
|
+
- KEY_REVOKED: the gateway closed the connection (4003) because the API key was revoked.
|
|
34
|
+
- ENTITLEMENTS_CHANGED: the gateway closed the connection (4003) because the account's rights no longer cover it (offer
|
|
35
|
+
ended, account suspended, subscription quota exceeded).
|
|
36
|
+
- TOO_MANY_CONNECTIONS: the connection quota of the plan stayed reached (4029) for `Reconnect.quota_retry`.
|
|
37
|
+
- PROTOCOL_ERROR, BINARY_NOT_SUPPORTED, INVALID_UTF8, MESSAGE_TOO_BIG: the gateway refused a message (close 1002, 1003,
|
|
38
|
+
1007, 1009); it would refuse it again, so the stream fails.
|
|
39
|
+
- ABORTED: kept for the other SDKs (a call stopped by its signal); in Python a call is stopped by cancelling its task
|
|
40
|
+
(`asyncio.timeout()`, `Task.cancel()`), and the `asyncio.CancelledError` goes through untouched, as asyncio requires.
|
|
41
|
+
- NETWORK_ERROR: the API could not be reached, or the connection was lost during an answer (DNS, refused or reset
|
|
42
|
+
connection, TLS); the error of httpx is the `__cause__`.
|
|
43
|
+
- `HTTP_<status>` (e.g. `HTTP_502`): an HTTP error answer without a problem document.
|
|
44
|
+
|
|
45
|
+
Every error the SDK raises or ends an iteration with is a `FathomChartsError`; the error it stems from, if any, is its `__cause__`.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ProblemFieldError(TypedDict):
|
|
50
|
+
"""One invalid field of a request: JSON Pointer from the root of the body, and what is wrong with it."""
|
|
51
|
+
|
|
52
|
+
path: str
|
|
53
|
+
message: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FathomChartsError(Exception):
|
|
57
|
+
"""Error raised by the SDK: a stable code (`ErrorCode`), the HTTP status or WebSocket close code when known."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
code: str,
|
|
62
|
+
message: str,
|
|
63
|
+
*,
|
|
64
|
+
status: int | None = None,
|
|
65
|
+
close_code: int | None = None,
|
|
66
|
+
retry_after: float | None = None,
|
|
67
|
+
problem: dict[str, Any] | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
super().__init__(message)
|
|
70
|
+
self.code = code
|
|
71
|
+
self.message = message
|
|
72
|
+
self.status = status
|
|
73
|
+
self.close_code = close_code
|
|
74
|
+
self.retry_after = retry_after
|
|
75
|
+
self.problem = problem
|
|
76
|
+
"""The RFC 9457 problem document of an HTTP error, with its `errors` (invalid fields, `ProblemFieldError`) when the API
|
|
77
|
+
gives them; None when the answer is not a problem document."""
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
return f"FathomChartsError({self.code!r}, {self.message!r})"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def api_key_problem(api_key: object) -> str | None:
|
|
84
|
+
"""Why `api_key` cannot be used (an unset or empty environment variable, typically), or None."""
|
|
85
|
+
if api_key is None:
|
|
86
|
+
return "api_key is missing: is the variable holding the API key set?"
|
|
87
|
+
if not isinstance(api_key, str):
|
|
88
|
+
return f"api_key must be a string, not {type(api_key).__name__}"
|
|
89
|
+
if api_key.strip() == "":
|
|
90
|
+
return "api_key is an empty string: is the variable holding the API key set and not empty?"
|
|
91
|
+
return None
|