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%
6.3 KB · 186 lines javascript
Raw Blame History
1/* ============================================================================2 * HFChart — couche données : API HF Market Data (https://www.hfmarketdata.io)3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * Le navigateur interroge l'API directement (CORS ouvert). Les timeframes6 * absents du lac sont dérivés côté client par resampling (1h→4h, 1J→1S,7 * 5min→15m, …). `/v1/status` pilote dynamiquement ce qui est disponible.8 * ==========================================================================*/9(function () {10  'use strict';11  const HF = (window.HF = window.HF || {});12  const API = window.HFMD_API || 'https://www.hfmarketdata.io';13  const MAX_API_LIMIT = 50000;1415  const GROUPS = [16    { key: 'index', label: 'Indices' },17    { key: 'etf', label: 'ETF' },18    { key: 'stock', label: 'Actions' },19    { key: 'futures', label: 'Futures' },20    { key: 'crypto', label: 'Crypto' },21    { key: 'fx', label: 'FX' },22  ];2324  /* tf UI → sources candidates (ordre de préférence) + facteur de resampling */25  const TF_SOURCES = {26    '1': [['1min', 1]],27    '5': [['5min', 1], ['1min', 5]],28    '15': [['5min', 3], ['1min', 15]],29    '30': [['30min', 1], ['5min', 6], ['1min', 30]],30    '60': [['1hour', 1], ['30min', 2], ['5min', 12]],31    '240': [['1hour', 4], ['30min', 8]],32    'D': [['1day', 1]],33    'W': [['1day', 'W']],34  };3536  let status = null; // /v1/status → { asset: { tf: { adj: n } } }3738  async function getJSON(url) {39    const res = await fetch(url);40    if (!res.ok) {41      let msg = res.statusText;42      try { msg = (await res.json()).detail || msg; } catch (e) { /* texte brut */ }43      throw new Error(msg);44    }45    return res.json();46  }4748  async function loadStatus() {49    if (status) return status;50    status = (await getJSON(`${API}/v1/status`)).datasets || {};51    return status;52  }5354  function srcAvailable(asset, srcTf) {55    const a = status && status[asset];56    if (!a || !a[srcTf]) return false;57    return Object.values(a[srcTf]).some(n => n > 0);58  }5960  /* Résout un tf UI vers (source, facteur) selon la disponibilité réelle. */61  function resolveTf(asset, uiTf) {62    for (const [src, factor] of (TF_SOURCES[uiTf] || [])) {63      if (srcAvailable(asset, src)) return { src, factor };64    }65    return null;66  }6768  function tfAvailability(asset) {69    const out = {};70    for (const tf of Object.keys(TF_SOURCES)) out[tf] = !!resolveTf(asset, tf);71    return out;72  }7374  function parseDT(s) { // "YYYY-MM-DD HH:MM:SS" → epoch (UTC naïf)75    return Date.UTC(+s.slice(0, 4), +s.slice(5, 7) - 1, +s.slice(8, 10),76      +(s.slice(11, 13) || 0), +(s.slice(14, 16) || 0), +(s.slice(17, 19) || 0)) / 1000;77  }7879  function resampleSec(bars, sec) {80    const out = [];81    let cur = null, key = null;82    for (const b of bars) {83      const k = b.t - (b.t % sec);84      if (k !== key) {85        if (cur) out.push(cur);86        key = k;87        cur = { t: k, o: b.o, h: b.h, l: b.l, c: b.c, v: b.v };88      } else {89        cur.h = Math.max(cur.h, b.h);90        cur.l = Math.min(cur.l, b.l);91        cur.c = b.c;92        cur.v += b.v;93      }94    }95    if (cur) out.push(cur);96    return out;97  }9899  function resampleWeekly(daily) {100    const out = [];101    let cur = null, key = null;102    for (const b of daily) {103      const wd = (Math.floor(b.t / 86400) + 3) % 7; // lundi = 0104      const k = b.t - wd * 86400;105      if (k !== key) {106        if (cur) out.push(cur);107        key = k;108        cur = { t: k, o: b.o, h: b.h, l: b.l, c: b.c, v: b.v };109      } else {110        cur.h = Math.max(cur.h, b.h);111        cur.l = Math.min(cur.l, b.l);112        cur.c = b.c;113        cur.v += b.v;114      }115    }116    if (cur) out.push(cur);117    return out;118  }119120  function detectDecimals(bars) {121    let dec = 0;122    for (let i = Math.max(0, bars.length - 80); i < bars.length; i++) {123      const c = bars[i].c;124      for (let d = dec; d <= 6; d++) {125        if (Math.abs(c * Math.pow(10, d) - Math.round(c * Math.pow(10, d))) < 1e-6) {126          dec = Math.max(dec, d);127          break;128        }129        if (d === 6) dec = 6;130      }131    }132    return dec;133  }134135  const tickerCache = new Map();136  async function tickers(asset) {137    if (tickerCache.has(asset)) return tickerCache.get(asset);138    await loadStatus();139    // n'importe quel tf disponible fait l'affaire pour lister140    const tfs = Object.keys(status[asset] || {});141    const tf = tfs.includes('1day') ? '1day' : tfs[0];142    if (!tf) return [];143    const j = await getJSON(`${API}/v1/${asset}/tickers?timeframe=${tf}&limit=20000`);144    const list = (j.tickers || []).filter(t => t && t.trim());145    tickerCache.set(asset, list);146    return list;147  }148149  const barCache = new Map();150  /* Retourne {parsed:[{t,o,h,l,c,v}], hasVolume, decimals, srcLimitReached} */151  async function bars(asset, ticker, uiTf, wantBars) {152    await loadStatus();153    const r = resolveTf(asset, uiTf);154    if (!r) throw new Error(`Timeframe indisponible pour ${asset} (lac de données)`);155    const factor = r.factor === 'W' ? 5 : r.factor;156    const apiLimit = Math.min(MAX_API_LIMIT, Math.ceil(wantBars * factor * 1.1));157    const key = `${asset}|${ticker}|${r.src}|${apiLimit}`;158    if (barCache.has(key)) return barCache.get(key);159    const j = await getJSON(160      `${API}/v1/bars/${asset}/${encodeURIComponent(ticker)}?timeframe=${r.src}&order=desc&limit=${apiLimit}`);161    const rows = (j.data || []).reverse();162    let parsed = rows.map(rec => ({163      t: parseDT(rec.datetime), o: rec.open, h: rec.high, l: rec.low, c: rec.close,164      v: rec.volume != null ? rec.volume : 0,165    }));166    if (r.factor === 'W') parsed = resampleWeekly(parsed);167    else if (r.factor > 1) {168      const secOf = { '1min': 60, '5min': 300, '30min': 1800, '1hour': 3600 };169      parsed = resampleSec(parsed, secOf[r.src] * r.factor);170    }171    const out = {172      parsed,173      hasVolume: rows.some(rec => rec.volume != null && rec.volume > 0),174      decimals: detectDecimals(parsed),175      srcLimitReached: (j.count || rows.length) >= apiLimit && apiLimit >= MAX_API_LIMIT,176      src: r.src,177    };178    if (barCache.size > 24) barCache.delete(barCache.keys().next().value);179    barCache.set(key, out);180    return out;181  }182183  HF.data = { API, GROUPS, loadStatus, tfAvailability, resolveTf, tickers, bars,184              resampleSec, resampleWeekly, MAX_API_LIMIT };185})();186