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.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .ruff_cache/
@@ -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