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 — 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