SPB Git

spb/hfchart Public

HFChart — la référence des charts haute fréquence : 14 types rendus canvas from scratch (zéro dépendance), données HF Market Data — www.hfchart.io

JavaScript 82.1% CSS 10% HTML 5.8% Python 2.1%

feat(options-lab): client API options — tickers/expirations/chaîne/history/spot, cache LRU, lazy par échéance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 13 h ago (Aug 10, 2026) parent fbf8d2a

Showing 1 changed file with +135 and −0

added web/js/options-data.js +135 −0
@@ -0,0 +1,135 @@
1 +/* ============================================================================
2 + * HFChart Options Lab — client API options (HF Market Data)
3 + * Author : Simon-Pierre Boucher — contact@spboucher.ai
4 + * ----------------------------------------------------------------------------
5 + * Extension du client HF.data existant pour les endpoints /v1/options/*.
6 + * Caches par (ticker, date) et (ticker, date, expiry) — les chaînes sont
7 + * volumineuses, le chargement est paresseux par échéance.
8 + * ==========================================================================*/
9 +(function () {
10 + 'use strict';
11 + const HF = (window.HF = window.HF || {});
12 + const API = window.HFMD_API || 'https://www.hfmarketdata.io';
13 +
14 + async function getJSON(url) {
15 + const res = await fetch(url);
16 + if (!res.ok) {
17 + let msg = res.statusText;
18 + try { msg = (await res.json()).detail || msg; } catch (e) { /* texte brut */ }
19 + const err = new Error(msg);
20 + err.status = res.status;
21 + throw err;
22 + }
23 + return res.json();
24 + }
25 +
26 + /* Cache LRU minimal partagé par toutes les requêtes options. */
27 + const cache = new Map();
28 + const CACHE_MAX = 60;
29 + async function cached(key, fetcher) {
30 + if (cache.has(key)) return cache.get(key);
31 + const value = await fetcher();
32 + if (cache.size >= CACHE_MAX) cache.delete(cache.keys().next().value);
33 + cache.set(key, value);
34 + return value;
35 + }
36 +
37 + /* Sous-jacents disponibles (dernier trimestre). */
38 + function tickers(search) {
39 + const q = search ? `&search=${encodeURIComponent(search.toUpperCase())}` : '';
40 + return cached(`tick|${search || ''}`, async () =>
41 + (await getJSON(`${API}/v1/options/tickers?limit=12000${q}`)).tickers || []);
42 + }
43 +
44 + /* Échéances disponibles pour un ticker à une date de marché donnée.
45 + * Retourne [] si le jour est férié / sans données. */
46 + function expirations(ticker, tradeDate) {
47 + const q = tradeDate ? `?trade_date=${tradeDate}` : '';
48 + return cached(`exp|${ticker}|${tradeDate || ''}`, async () => {
49 + try {
50 + return (await getJSON(`${API}/v1/options/expirations/${encodeURIComponent(ticker)}${q}`)).expirations || [];
51 + } catch (e) {
52 + if (e.status === 404) return [];
53 + throw e;
54 + }
55 + });
56 + }
57 +
58 + /* Chaîne d'une seule échéance à une date donnée (chargement paresseux).
59 + * Sans tradeDate, l'API renvoie la dernière date disponible. */
60 + function chain(ticker, tradeDate, expiry) {
61 + const params = new URLSearchParams({ limit: '6000' });
62 + if (tradeDate) params.set('trade_date', tradeDate);
63 + if (expiry) params.set('expiry', expiry);
64 + return cached(`chain|${ticker}|${tradeDate || 'latest'}|${expiry || 'all'}`, async () => {
65 + try {
66 + const j = await getJSON(`${API}/v1/options/chain/${encodeURIComponent(ticker)}?${params}`);
67 + return j.data || [];
68 + } catch (e) {
69 + if (e.status === 404) return [];
70 + throw e;
71 + }
72 + });
73 + }
74 +
75 + /* Dernière date de marché disponible pour un ticker (sonde légère). */
76 + function latestTradeDate(ticker) {
77 + return cached(`latest|${ticker}`, async () => {
78 + const rows = await chain(ticker, null, null);
79 + return rows.length ? rows[0].trade_date : null;
80 + });
81 + }
82 +
83 + /* Série journalière complète d'un contrat (pour le replay mark-to-market). */
84 + function history(ticker, strike, expiry, callPut) {
85 + const params = new URLSearchParams({
86 + strike: String(strike), expiry, call_put: callPut, limit: '5000',
87 + });
88 + return cached(`hist|${ticker}|${strike}|${expiry}|${callPut}`, async () => {
89 + try {
90 + const j = await getJSON(`${API}/v1/options/history/${encodeURIComponent(ticker)}?${params}`);
91 + return j.data || [];
92 + } catch (e) {
93 + if (e.status === 404) return [];
94 + throw e;
95 + }
96 + });
97 + }
98 +
99 + const SPOT_ASSETS = ['etf', 'stock', 'index'];
100 +
101 + /* Barres journalières du sous-jacent sur [start, end] — la classe d'actif
102 + * n'est pas connue : essais etf → stock → index (mémorisé par ticker). */
103 + const assetOf = new Map();
104 + async function spotBars(ticker, start, end) {
105 + const tryAssets = assetOf.has(ticker) ? [assetOf.get(ticker)] : SPOT_ASSETS;
106 + for (const asset of tryAssets) {
107 + try {
108 + const params = new URLSearchParams({
109 + timeframe: '1day', order: 'asc', limit: '4000', start, end,
110 + });
111 + const j = await getJSON(`${API}/v1/bars/${asset}/${encodeURIComponent(ticker)}?${params}`);
112 + if (j.data && j.data.length) {
113 + assetOf.set(ticker, asset);
114 + return { asset, bars: j.data };
115 + }
116 + } catch (e) {
117 + if (e.status !== 404) throw e;
118 + }
119 + }
120 + return { asset: null, bars: [] };
121 + }
122 +
123 + /* Spot de clôture à une date donnée (dernière barre ≤ date). */
124 + async function spotOn(ticker, dateISO) {
125 + const from = new Date(dateISO + 'T00:00:00Z');
126 + from.setUTCDate(from.getUTCDate() - 10); // marge pour week-ends/fériés
127 + const start = from.toISOString().slice(0, 10);
128 + const { asset, bars } = await spotBars(ticker, start, dateISO + ' 23:59:59');
129 + if (!bars.length) return null;
130 + const last = bars[bars.length - 1];
131 + return { asset, date: String(last.datetime).slice(0, 10), close: last.close };
132 + }
133 +
134 + HF.optdata = { tickers, expirations, chain, latestTradeDate, history, spotBars, spotOn, API };
135 +})();
136