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%
5.5 KB · 140 lines javascript
Raw Blame History
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