/* ============================================================================ * HFChart Options Lab — client API options (HF Market Data) * Author : Simon-Pierre Boucher — contact@spboucher.ai * ---------------------------------------------------------------------------- * Extension du client HF.data existant pour les endpoints /v1/options/*. * Caches par (ticker, date) et (ticker, date, expiry) — les chaînes sont * volumineuses, le chargement est paresseux par échéance. * ==========================================================================*/ (function () { 'use strict'; const HF = (window.HF = window.HF || {}); const API = window.HFMD_API || 'https://www.hfmarketdata.io'; async function getJSON(url) { const res = await fetch(url); if (!res.ok) { let msg = res.statusText; try { msg = (await res.json()).detail || msg; } catch (e) { /* texte brut */ } const err = new Error(msg); err.status = res.status; throw err; } return res.json(); } /* Cache LRU minimal partagé par toutes les requêtes options. */ const cache = new Map(); const CACHE_MAX = 60; async function cached(key, fetcher) { if (cache.has(key)) return cache.get(key); const value = await fetcher(); if (cache.size >= CACHE_MAX) cache.delete(cache.keys().next().value); cache.set(key, value); return value; } /* Sous-jacents disponibles (dernier trimestre). */ function tickers(search) { const q = search ? `&search=${encodeURIComponent(search.toUpperCase())}` : ''; return cached(`tick|${search || ''}`, async () => (await getJSON(`${API}/v1/options/tickers?limit=12000${q}`)).tickers || []); } /* Échéances disponibles pour un ticker à une date de marché donnée. * Retourne [] si le jour est férié / sans données. */ function expirations(ticker, tradeDate) { const q = tradeDate ? `?trade_date=${tradeDate}` : ''; return cached(`exp|${ticker}|${tradeDate || ''}`, async () => { try { return (await getJSON(`${API}/v1/options/expirations/${encodeURIComponent(ticker)}${q}`)).expirations || []; } catch (e) { if (e.status === 404) return []; throw e; } }); } /* Chaîne d'une seule échéance à une date donnée (chargement paresseux). * Sans tradeDate, l'API renvoie la dernière date disponible. */ function chain(ticker, tradeDate, expiry) { const params = new URLSearchParams({ limit: '6000' }); if (tradeDate) params.set('trade_date', tradeDate); if (expiry) params.set('expiry', expiry); return cached(`chain|${ticker}|${tradeDate || 'latest'}|${expiry || 'all'}`, async () => { try { const j = await getJSON(`${API}/v1/options/chain/${encodeURIComponent(ticker)}?${params}`); return j.data || []; } catch (e) { if (e.status === 404) return []; throw e; } }); } /* Dernière date de marché disponible pour un ticker (sonde légère). */ function latestTradeDate(ticker) { return cached(`latest|${ticker}`, async () => { const rows = await chain(ticker, null, null); return rows.length ? rows[0].trade_date : null; }); } /* Série journalière complète d'un contrat (pour le replay mark-to-market). */ function history(ticker, strike, expiry, callPut) { const params = new URLSearchParams({ strike: String(strike), expiry, call_put: callPut, limit: '5000', }); return cached(`hist|${ticker}|${strike}|${expiry}|${callPut}`, async () => { try { const j = await getJSON(`${API}/v1/options/history/${encodeURIComponent(ticker)}?${params}`); return j.data || []; } catch (e) { if (e.status === 404) return []; throw e; } }); } const SPOT_ASSETS = ['etf', 'stock', 'index']; /* Barres journalières du sous-jacent sur [start, end] — la classe d'actif * n'est pas connue : essais etf → stock → index (mémorisé par ticker). */ const assetOf = new Map(); async function spotBars(ticker, start, end) { const tryAssets = assetOf.has(ticker) ? [assetOf.get(ticker)] : SPOT_ASSETS; for (const asset of tryAssets) { try { const params = new URLSearchParams({ timeframe: '1day', order: 'asc', limit: '4000', start, end, }); // les strikes/primes d'options sont as-traded : il faut le spot NON // ajusté des splits/dividendes (sinon AAPL 2015 vaut 23 $ face à des // strikes 105/110) if (asset === 'stock' || asset === 'etf') params.set('adjustment', 'UNADJUSTED'); const j = await getJSON(`${API}/v1/bars/${asset}/${encodeURIComponent(ticker)}?${params}`); if (j.data && j.data.length) { assetOf.set(ticker, asset); return { asset, bars: j.data }; } } catch (e) { if (e.status !== 404) throw e; } } return { asset: null, bars: [] }; } /* Spot de clôture à une date donnée (dernière barre ≤ date). */ async function spotOn(ticker, dateISO) { const from = new Date(dateISO + 'T00:00:00Z'); from.setUTCDate(from.getUTCDate() - 10); // marge pour week-ends/fériés const start = from.toISOString().slice(0, 10); const { asset, bars } = await spotBars(ticker, start, dateISO + ' 23:59:59'); if (!bars.length) return null; const last = bars[bars.length - 1]; return { asset, date: String(last.datetime).slice(0, 10), close: last.close }; } HF.optdata = { tickers, expirations, chain, latestTradeDate, history, spotBars, spotOn, API }; })();