# SDK TypeScript

`@fathom-charts/sdk` gère pour vous l’authentification, le snapshot initial, la reprise après coupure, les doublons, la pagination de l’historique, les exports et les retries du rate limit. Cette page décrit toutes ses options et méthodes.

## Installer

```bash
npm install @fathom-charts/sdk ws
```

- **ESM uniquement** : le package ne fournit pas de build CommonJS. Votre projet doit déclarer `"type": "module"` dans son `package.json` (ou utiliser des fichiers `.mjs` / `.mts`).
- **Node 22 ou plus récent**, et les navigateurs. `exportNdjson()` demande Node 22.15 ou plus récent (décompression zstd de `node:zlib`).
- **`ws`** n’est utile qu’en Node : il ouvre la connexion avec le header `Authorization`. Dans un navigateur, le SDK utilise le `WebSocket` natif avec un [stream token](https://fathomcharts.com/docs/authentification.md#accès-depuis-un-navigateur), et n’importe jamais `ws`.

Le SDK importe `ws` dynamiquement, avec un nom de module dans une variable, pour que les bundlers navigateur ne l’embarquent pas. Webpack signale alors `Critical dependency: the request of a dependency is an expression` : cet avertissement est sans conséquence.

### Types des indicateurs

Le SDK est générique : les données d’un indicateur sont typées par les types que vous générez depuis le catalogue (`GET /v1/indicators`, sans authentification).

```bash
curl https://api.fathomcharts.com/v1/indicators -o catalog.json
npx fathom-charts-types catalog.json src/fathom-charts-types.ts
```

Le fichier généré exporte, pour chaque indicateur, `<Nom>Params` et `<Nom>Data` (`BigTradesData`, `FootprintData`…), ainsi que `IndicatorParams`, `IndicatorData` et `IndicatorId`. Les descriptions du catalogue deviennent des commentaires JSDoc ; un niveau de footprint est typé `[number, number, number, number?]` (le 4e élément est facultatif). Le dossier de sortie est créé s’il n’existe pas. Régénérez le fichier quand le catalogue change (son `engineFingerprint` change avec le calcul).

## Temps réel : FathomChartsStream

```ts
import { FathomChartsStream } from '@fathom-charts/sdk';
import type { BigTradesData } from './fathom-charts-types.js';

const stream = new FathomChartsStream({
  url: 'wss://stream.fathomcharts.com/v1',
  apiKey: process.env.FATHOM_CHARTS_API_KEY!,
  onStateChange: (state) => console.log('connexion', state),
});

const sub = stream.subscribe<BigTradesData>({
  instrument: 'NQ.front',
  indicator: 'big-trades',
  params: { minimum: 30 },
  mode: 'confirmed',
});

for await (const event of sub) {
  if (event.type === 'upsert') console.log(event.cursor, event.data.side, event.data.volume);
}
```

### Options

| Option               | Rôle                                                                                                                                                                                                                                                                                      |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`                | Obligatoire. L’adresse du stream : `wss://stream.fathomcharts.com/v1`.                                                                                                                                                                                                                    |
| `apiKey`             | Clé API, envoyée dans le header `Authorization: Bearer`. Côté serveur seulement ; demande le package `ws`, sauf si vous fournissez `webSocket`.                                                                                                                                           |
| `streamToken`        | Dans un navigateur, à la place de `apiKey` : une fonction `async () => string` qui demande un stream token à votre serveur. Elle est appelée à chaque connexion, reconnexions comprises. Donnez exactement l’un de `apiKey` et `streamToken`, sinon le constructeur lève `CONFIGURATION`. |
| `webSocket`          | Facultatif : une fonction `(url, headers) => WebSocket` qui ouvre la connexion, pour utiliser une autre implémentation que `ws` ou le `WebSocket` du navigateur. `headers` contient le header `Authorization` avec `apiKey`, `undefined` avec `streamToken`.                              |
| `reconnect`          | Facultatif : `initialDelayMs` (premier délai de reconnexion, 250 ms par défaut), `maxDelayMs` (plafond du backoff, 30 s), `quotaRetryMs` (durée pendant laquelle un close code `4029` est réessayé, 120 s), `quotaDelayMs` (délai minimal entre deux essais après un `4029`, 5 s).        |
| `heartbeatTimeoutMs` | Silence au-delà duquel la connexion est considérée comme perdue, puis rouverte. Par défaut 15 000 ms : le serveur envoie un `hb` toutes les 5 secondes.                                                                                                                                   |
| `highWaterMark`      | Nombre d’events en attente de lecture, toutes subscriptions confondues, au-delà duquel le SDK ralentit la lecture du socket (avec `ws`) et ne garde que la dernière version de chaque objet en cours. Par défaut 10 000.                                                                  |
| `maxBuffered`        | Quand le socket ne peut pas être mis en pause (navigateur) : nombre d’events en attente au-delà duquel le SDK ferme la connexion, puis la rouvre au dernier cursor reçu une fois la moitié de `highWaterMark` lue. Rien n’est perdu. Par défaut 4 × `highWaterMark`.                      |
| `onStateChange`      | Appelée à chaque changement de `connectionState`.                                                                                                                                                                                                                                         |
| `onNotice`           | Appelée avec chaque message `notice` du serveur (`{ kind, effectiveAt? }`), par exemple l’annonce d’une maintenance (`kind: "reconnect"`). Le SDK se reconnecte de lui-même.                                                                                                              |

### subscribe()

`stream.subscribe<D>(options)` ouvre une subscription et renvoie un objet `Subscription`, à lire avec `for await`. Ses options sont celles du message [`subscribe`](https://fathomcharts.com/docs/websocket.md#sabonner) : `instrument`, `indicator`, `params` (`{}` par défaut), `mode` (`live` par défaut) et `from` (`"live"` par défaut, `{ cursor }` ou `{ time }`). Le SDK choisit lui-même le nom `sub`. Le type `D` est celui des données de l’indicateur (`BigTradesData`…).

La boucle reçoit des events dont le champ `type` reprend le nom du message du serveur, sans le champ `sub` :

| `type`       | Champs                                               | Quand                                                                                                                                            |
| ------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `subscribed` | `topic`, `instrument`, `tickSizeNanos`               | La subscription est acceptée. Arrive de nouveau après chaque reconnexion, et après un `reset` de raison `roll` ou `entitlement_changed`.         |
| `snapshot`   | `cursor`, `items` (chacun `{ id, final, ts, data }`) | L’état de départ. Remplacez tout votre état local par `items`.                                                                                   |
| `upsert`     | `cursor`, `id`, `final`, `ts`, `data`                | L’objet `id` est créé ou remplacé entièrement.                                                                                                   |
| `remove`     | `cursor`, `id`, `final`, `ts`                        | L’objet `id` est supprimé.                                                                                                                       |
| `status`     | `state`, `lagMs`                                     | L’état du stream (voir [État du stream](https://fathomcharts.com/docs/websocket.md#état-du-stream)). Arrive de nouveau après chaque reconnexion. |
| `reset`      | `reason`                                             | Votre état local n’est plus valable : jetez-le, un `snapshot` suit.                                                                              |

Les autres messages du serveur ne deviennent pas des events : le SDK traite lui-même les `hb` (détection d’une connexion muette, cursor de reprise), les `pong` et les `notice` (transmis à `onNotice`). Un `error` sur la subscription arrête la boucle : `for await` lève une `FathomChartsError` qui porte le `code` reçu (`INVALID_PARAMETERS`, `UNKNOWN_INSTRUMENT`, `FORBIDDEN`, `QUOTA_EXCEEDED`, `NOT_COVERED`…).

Le SDK ignore les doublons : après une reprise, aucun `upsert` ou `remove` dont le cursor n’est pas strictement supérieur au dernier reçu ne vous est remis.

### Subscription

| Membre    | Rôle                                                                                                                     |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| `cursor`  | Le cursor du dernier event qui vous a été remis : votre point de reprise. `undefined` tant qu’aucun cursor n’est arrivé. |
| `topic`   | Le `topic` du dernier `subscribed`.                                                                                      |
| `close()` | Arrête la subscription : le SDK envoie `unsubscribe`, et une boucle en attente se termine.                               |

Sortir de la boucle (`break`, `return` ou une exception dans son corps) appelle `close()`.

### Cycle de vie de la connexion

Toutes les subscriptions d’un `FathomChartsStream` partagent une seule connexion, qui compte dans le nombre de connexions de votre offre :

- la connexion s’ouvre au premier `subscribe()` ;
- quand la dernière subscription se ferme (`break`, `close()`, ou un `error` du serveur sur la dernière subscription), le SDK ferme la connexion (close code `1000`, raison `idle`) et `connectionState` passe à `idle`. Un nouveau `subscribe()` la rouvre ;
- `stream.close()` ferme toutes les subscriptions et la connexion. L’appeler juste après `subscribe()`, avant que la connexion soit ouverte, est sans risque. Un `subscribe()` après `close()` lève `CLOSED` ;
- `connectionState` vaut `idle`, `connecting`, `open`, `reconnecting` ou `closed`.

Après une coupure, le SDK se reconnecte avec un backoff exponentiel et du jitter, et renvoie chaque `subscribe` avec `from: { cursor }`, le dernier cursor reçu : vous recevez de nouveau `subscribed` et `status`, puis les messages manqués, sans snapshot si la reprise réussit, ou un `reset` suivi d’un snapshot sinon. Une subscription `from: { time }` qui a déjà reçu un cursor reprend à ce cursor : la période passée n’est jamais rejouée une seconde fois. Seule une coupure pendant `warming`, avant le premier cursor, la relance depuis le `time` d’origine, et la partie rejouée depuis l’historique est alors [décomptée](https://fathomcharts.com/docs/websocket.md#démarrer-dans-le-passé) de nouveau.

| Close code                                   | Ce que fait le SDK                                                                                                                                                        |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `4001`                                       | Arrête toutes les subscriptions avec `UNAUTHORIZED`, sans se reconnecter.                                                                                                 |
| `4003`                                       | Arrête toutes les subscriptions avec `KEY_REVOKED`, `ENTITLEMENTS_CHANGED` ou `FORBIDDEN`, selon la raison de la fermeture, sans se reconnecter.                          |
| `1002`, `1003`, `1007`, `1009`               | Arrête toutes les subscriptions avec `PROTOCOL_ERROR`, `BINARY_NOT_SUPPORTED`, `INVALID_UTF8` ou `MESSAGE_TOO_BIG` : les mêmes messages seraient refusés de nouveau.      |
| `4029`                                       | Réessaie pendant `reconnect.quotaRetryMs` (une connexion perdue sans fermeture expire côté serveur au bout de 90 secondes), puis arrête tout avec `TOO_MANY_CONNECTIONS`. |
| `4008`                                       | Se reconnecte immédiatement et reprend au dernier cursor.                                                                                                                 |
| Autres, ou `heartbeatTimeoutMs` sans message | Se reconnecte avec backoff (au moins 1 seconde après un `1013`).                                                                                                          |

Après une de ces erreurs définitives, un nouveau `subscribe()` sur le même `FathomChartsStream` lève la même erreur : corrigez la cause, puis créez un nouveau `FathomChartsStream`.

### Reprendre après un redémarrage

Le SDK reprend seul après une coupure, tant que votre process tourne. Pour reprendre après un redémarrage de votre process, enregistrez `sub.cursor` au fil de l’eau (avec l’état que vous en avez tiré), puis rouvrez la subscription avec `from: { cursor }` :

```ts
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { FathomChartsStream } from '@fathom-charts/sdk';
import type { BigTradesData } from './fathom-charts-types.js';

// Ici un fichier ; en production, le stockage de votre état (base de données…).
const CURSOR_FILE = 'big-trades.cursor';
const saved = existsSync(CURSOR_FILE) ? readFileSync(CURSOR_FILE, 'utf8') : undefined;

const stream = new FathomChartsStream({
  url: 'wss://stream.fathomcharts.com/v1',
  apiKey: process.env.FATHOM_CHARTS_API_KEY!,
});

const sub = stream.subscribe<BigTradesData>({
  instrument: 'NQ.front',
  indicator: 'big-trades',
  params: { minimum: 30 },
  mode: 'confirmed',
  from: saved ? { cursor: saved } : 'live',
});

for await (const event of sub) {
  if (event.type === 'upsert') console.log(event.data.side, event.data.volume);
  // Le cursor est enregistré une fois l’event traité.
  if (sub.cursor) writeFileSync(CURSOR_FILE, sub.cursor);
}
```

Si le cursor est trop ancien, vous recevez un `reset` puis un nouveau snapshot : repartez de celui-ci.

## Historique : FathomChartsRest

```ts
import { FathomChartsRest } from '@fathom-charts/sdk';

const rest = new FathomChartsRest({ baseUrl: 'https://api.fathomcharts.com', apiKey: process.env.FATHOM_CHARTS_API_KEY! });
```

| Option             | Rôle                                                                                          |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `baseUrl`          | Obligatoire. L’adresse de l’API : `https://api.fathomcharts.com`.                             |
| `apiKey`           | Obligatoire. Clé API, envoyée dans le header `Authorization: Bearer`. Côté serveur seulement. |
| `fetch`            | Facultatif : une implémentation de `fetch` à utiliser à la place de celle de la plateforme.   |
| `rateLimitRetries` | Nombre de nouvelles tentatives après un `429 RATE_LIMITED`. Par défaut 3.                     |

### Méthodes

Chaque méthode qui interroge l’historique prend la requête décrite sur la page [Historique](https://fathomcharts.com/docs/historique.md#faire-une-requête) (`instrument`, `indicator`, `params`, `from`, `to`, `mode`, et selon la méthode `limit`, `cursor`, `untilCursor`, `snapshot`), et un `AbortSignal` facultatif.

| Méthode                         | Renvoie                                                                                                                                                                                                                                         |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page(query, signal?)`          | Une page : `{ instrument, tickSizeNanos, snapshot?, items, next, computeUnits }`.                                                                                                                                                               |
| `pages(query, signal?)`         | Les pages une par une, en suivant `next`, au fil de votre lecture.                                                                                                                                                                              |
| `history(query, signal?)`       | Les résultats un par un, toutes pages confondues. Avec `snapshot: true`, le premier élément est l’état à `from`, `{ t: 'snapshot', cursor, items }`, puis viennent les mises à jour (`t` vaut `upsert` ou `remove`, chacune avec son `cursor`). |
| `estimate(query, signal?)`      | L’[estimation](https://fathomcharts.com/docs/historique.md#estimer-le-coût) du coût, sans rien décompter. Accepte la même requête que `page()`.                                                                                                 |
| `export(query, signal?)`        | Une réponse d’[export](https://fathomcharts.com/docs/exports.md) : `{ body, contentType, filename, token }`. `body` est le fichier compressé en zstd tel quel ; `token` reprend l’export avec `resume`.                                         |
| `exportNdjson(query, options?)` | L’export décompressé, en morceaux (`Uint8Array`) de lignes complètes, avec reprise automatique. Voir [Exports](https://fathomcharts.com/docs/exports.md#avec-le-sdk).                                                                           |
| `streamToken()`                 | Un stream token pour navigateur : `{ token, expiresAt }`.                                                                                                                                                                                       |
| `usage()`                       | Votre [consommation](https://fathomcharts.com/docs/limites.md#suivre-votre-consommation) du mois et vos limites.                                                                                                                                |
| `instruments()`                 | `{ instruments }` : chaque [instrument](https://fathomcharts.com/docs.md#instruments) servi, avec `symbol`, `alias`, `tickSizeNanos`, `status` et `updatedAt`.                                                                                  |
| `status()`                      | `{ engine: { state }, instruments }` : l’état du calcul (`up` ou `down`) et celui de chaque instrument.                                                                                                                                         |
| `offers()`                      | Les offres et les limites de chaque combinaison (`GET /v1/offers`).                                                                                                                                                                             |
| `indicators()`                  | Le catalogue des indicateurs : `{ version, engineFingerprint, indicators }`.                                                                                                                                                                    |

Les options de `exportNdjson()` : `signal`, `maxResumes` (nombre de reprises consécutives sans nouvelle ligne avant d’abandonner, 5 par défaut) et `resumeDelayMs` (délai avant la première reprise, 1 s par défaut, doublé à chaque reprise consécutive jusqu’à 30 s). Si la toute première requête échoue (erreur réseau ou `5xx` avant le header `X-Export-Token`), `exportNdjson()` ne la renvoie jamais : un nouvel export serait facturé de nouveau. Il lève l’erreur, et vous décidez.

### Rate limit et retries

Un `429 RATE_LIMITED` est réessayé jusqu’à `rateLimitRetries` fois : après le délai du header `Retry-After` (60 secondes au plus), ou, sans ce header, après 1, 2, puis 4 secondes. Un `AbortSignal` interrompt l’attente immédiatement. `429 QUOTA_EXCEEDED` (budget du mois épuisé) n’est jamais réessayé : il est levé tout de suite.

## Erreurs

Toutes les erreurs du SDK sont des `FathomChartsError` :

| Champ                | Contenu                                                                                                                                                                                                                                                                |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`               | Le code stable de l’erreur : un code de l’API (voir [Erreurs](https://fathomcharts.com/docs/errors.md#liste-des-codes)), un code propre au SDK (ci-dessous), ou `HTTP_<status>` (`HTTP_502`, par exemple) pour une réponse HTTP d’erreur sans document `problem+json`. |
| `message`            | Une explication lisible. En REST : le `detail` de la réponse, suivi de chaque champ invalide (`<detail>: <path> <message>; …`).                                                                                                                                        |
| `details.status`     | Le statut HTTP, en REST.                                                                                                                                                                                                                                               |
| `details.closeCode`  | Le close code WebSocket, quand la connexion a été fermée.                                                                                                                                                                                                              |
| `details.retryAfter` | La valeur du header `Retry-After`, en secondes, quand la réponse en a un.                                                                                                                                                                                              |
| `details.problem`    | Le document `problem+json` complet de la réponse, avec `errors` quand l’API liste des champs invalides.                                                                                                                                                                |

Codes propres au SDK, en plus de ceux de l’API (la liste complète est exportée dans `ERROR_CODES`) :

| `code`                                                                      | Cause                                                                                                                                                                                                                                                        |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CONFIGURATION`                                                             | Options invalides (`apiKey` et `streamToken` ensemble, ou aucun des deux), ou fonctionnalité absente de la plateforme : package `ws` manquant pour une clé API, aucune implémentation de `WebSocket`, décompression zstd indisponible pour `exportNdjson()`. |
| `CLOSED`                                                                    | `subscribe()` sur un `FathomChartsStream` fermé par `close()`.                                                                                                                                                                                               |
| `KEY_REVOKED`                                                               | La connexion a été fermée (`4003`) car la clé a été révoquée ou a expiré.                                                                                                                                                                                    |
| `ENTITLEMENTS_CHANGED`                                                      | La connexion a été fermée (`4003`) car vos offres ne la couvrent plus (offre terminée, compte suspendu, quota de subscriptions dépassé).                                                                                                                     |
| `TOO_MANY_CONNECTIONS`                                                      | Le nombre maximal de connexions de votre offre est resté atteint (`4029`) pendant `reconnect.quotaRetryMs`.                                                                                                                                                  |
| `PROTOCOL_ERROR`, `BINARY_NOT_SUPPORTED`, `INVALID_UTF8`, `MESSAGE_TOO_BIG` | Le serveur a refusé un message envoyé par le client (close codes `1002`, `1003`, `1007`, `1009`).                                                                                                                                                            |
| `HTTP_<status>`                                                             | Réponse HTTP d’erreur sans document `problem+json`.                                                                                                                                                                                                          |

Une fermeture `4001` donne `UNAUTHORIZED`, avec un message qui rappelle les causes possibles (clé absente, mal formée, inconnue, révoquée ou expirée, stream token invalide, expiré ou déjà utilisé).

## Cursors

`compareCursors(a, b)` compare deux cursors `<utcDay>.<rank>.<k>` nombre par nombre et renvoie un nombre négatif, zéro ou positif. Un cursor mal formé (nombres avec des zéros en tête, champ manquant…) lève `INVALID_PARAMETERS`.

```ts
import { compareCursors } from '@fathom-charts/sdk';

compareCursors('20720.111416.0', '20720.111417.0'); // < 0
```
