Metadata-Version: 2.5
Name: fathom-charts
Version: 0.2.0
Summary: Fathom Charts client: real-time order-flow indicator streams (WebSocket) and history (REST), for Python 3.11+ (asyncio).
Project-URL: Homepage, https://fathomcharts.com/docs/sdk/python
Project-URL: Documentation, https://fathomcharts.com/docs/sdk/python
Author-email: Adrien Insights <support@fathomcharts.com>
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Keywords: fathom-charts,footprint,indicators,market-data,order-flow,trading,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.28
Requires-Dist: websockets>=15
Requires-Dist: zstandard>=0.23
Description-Content-Type: text/markdown

# fathom-charts

Python client for [Fathom Charts](https://fathomcharts.com): real-time order-flow indicator streams (WebSocket) and history (REST), for Python 3.11+ (asyncio).

The SDK handles authentication, the initial snapshot, resuming after a disconnect, duplicates, history pagination and rate-limit retries.

Documentation: [fathomcharts.com/docs/sdk/python](https://fathomcharts.com/docs/sdk/python)

## Install

```bash
pip install fathom-charts
# or
uv add fathom-charts
```

Generate the types of each indicator's parameters and data from the catalog (`GET /v1/indicators`, no authentication):

```bash
curl https://api.fathomcharts.com/v1/indicators -o catalog.json
fathom-charts-types catalog.json fathom_charts_types.py
```

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.

## Real time

```python
import asyncio
import os

from fathom_charts import FathomChartsStream, Subscription, Upsert
from fathom_charts_types import BigTradesData


async def main() -> None:
    async with FathomChartsStream(url="wss://stream.fathomcharts.com/v1", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as stream:
        big_trades: Subscription[BigTradesData]
        async with stream.subscribe(
            instrument="NQ.front",
            indicator="big-trades",
            params={"minimum": 30},
            mode="confirmed",
        ) as big_trades:
            async for event in big_trades:
                if isinstance(event, Upsert):
                    print(event.data["side"], event.data["volume"], event.data["price"])


asyncio.run(main())
```

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).

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.

## History

```python
from fathom_charts import FathomChartsRest

async with FathomChartsRest(base_url="https://api.fathomcharts.com", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as rest:
    async for m in rest.history({
        "instrument": "NQZ6",
        "indicator": "big-trades",
        "params": {"minimum": 30},
        "from": "1790256600000000000",
        "to": "1790258100000000000",
    }):
        if m["t"] == "upsert" and m["final"] and "data" in m:
            print(m["cursor"], m["data"]["side"], m["data"]["volume"])
```

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).

## Export to a file

`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:

```python
from contextlib import aclosing

from fathom_charts import RangeQuery

query: RangeQuery = {"instrument": "NQZ6", "indicator": "big-trades", "params": {"minimum": 30}, "from": "1790256600000000000", "to": "1790258100000000000"}
async with FathomChartsRest(base_url="https://api.fathomcharts.com", api_key=os.environ["FATHOM_CHARTS_API_KEY"]) as rest:
    with open("big-trades.ndjson", "wb") as file:
        async with aclosing(rest.export_ndjson(query)) as chunks:
            async for chunk in chunks:
                file.write(chunk)
```

`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).

## Connection, resume and flow control

`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).

Durations are in seconds.

- 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.
- `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.
- `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.
- `heartbeat_timeout` (default 15 s): without any message for this long (the server sends `hb` every 5 s), the connection is dropped and reopened.
- `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).
- `random`: source of the reconnection jitter, in [0, 1) (default `random.random`; for tests).
- `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`.

A `Subscription` is an async iterator of events, with:

- `id`: its `sub` on the connection (`s1`, `s2`…), as in the protocol messages;
- `request`: the subscription as requested, defaults filled in (`params={}`, `mode='live'`, `from_='live'`);
- `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;
- `topic`: topic key from the latest `subscribed`;
- `engine`: engine build from the latest `subscribed`; save it with `cursor` and resume a later process with `from_={"cursor": cursor, "engine": engine}`;
- `close()`: unsubscribes (as leaving its `async with` block does).

`CloseCode` lists the gateway's WebSocket close codes; `compare_cursors(a, b)` orders two cursors (negative, zero, positive).

## Errors

Every error the SDK raises, or ends an iteration with, is a `FathomChartsError` (`ERROR_CODES` lists the codes):

- `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;
- `status`, `close_code`, `retry_after` when known;
- `problem`: the RFC 9457 problem document of an HTTP error, with its `errors` (invalid fields); `None` when the answer is not a problem document;
- `__cause__`: the original error (the error of httpx…), when there is one.

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.

## License

Proprietary: you may use this package and include it in your own applications only to access the Fathom Charts service. See [LICENSE](./LICENSE).
