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%
1/* ============================================================================2 * HFChart Options Lab — client API options (HF Market Data)3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * Extension du client HF.data existant pour les endpoints /v1/options/*.6 * Caches par (ticker, date) et (ticker, date, expiry) — les chaînes sont7 * 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';1314 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 }2526 /* 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 }3637 /* 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 }4344 /* É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 }5758 /* 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 }7475 /* 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 }8283 /* 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 }9899 const SPOT_ASSETS = ['etf', 'stock', 'index'];100101 /* Barres journalières du sous-jacent sur [start, end] — la classe d'actif102 * 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 // les strikes/primes d'options sont as-traded : il faut le spot NON112 // ajusté des splits/dividendes (sinon AAPL 2015 vaut 23 $ face à des113 // strikes 105/110)114 if (asset === 'stock' || asset === 'etf') params.set('adjustment', 'UNADJUSTED');115 const j = await getJSON(`${API}/v1/bars/${asset}/${encodeURIComponent(ticker)}?${params}`);116 if (j.data && j.data.length) {117 assetOf.set(ticker, asset);118 return { asset, bars: j.data };119 }120 } catch (e) {121 if (e.status !== 404) throw e;122 }123 }124 return { asset: null, bars: [] };125 }126127 /* Spot de clôture à une date donnée (dernière barre ≤ date). */128 async function spotOn(ticker, dateISO) {129 const from = new Date(dateISO + 'T00:00:00Z');130 from.setUTCDate(from.getUTCDate() - 10); // marge pour week-ends/fériés131 const start = from.toISOString().slice(0, 10);132 const { asset, bars } = await spotBars(ticker, start, dateISO + ' 23:59:59');133 if (!bars.length) return null;134 const last = bars[bars.length - 1];135 return { asset, date: String(last.datetime).slice(0, 10), close: last.close };136 }137138 HF.optdata = { tickers, expirations, chain, latestTradeDate, history, spotBars, spotOn, API };139})();140