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 — indicateurs techniques, calculés from scratch3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * Toutes les fonctions retournent des tableaux alignés sur l'index des barres6 * (null avant la période de chauffe).7 * ==========================================================================*/8(function () {9 'use strict';10 const HF = (window.HF = window.HF || {});1112 const closes = bars => bars.map(b => b.c);1314 function sma(vals, n) {15 const out = new Array(vals.length).fill(null);16 let sum = 0;17 for (let i = 0; i < vals.length; i++) {18 sum += vals[i];19 if (i >= n) sum -= vals[i - n];20 if (i >= n - 1) out[i] = sum / n;21 }22 return out;23 }2425 function ema(vals, n) {26 const out = new Array(vals.length).fill(null);27 const k = 2 / (n + 1);28 let prev = null, seed = 0;29 for (let i = 0; i < vals.length; i++) {30 if (i < n - 1) { seed += vals[i]; continue; }31 if (i === n - 1) { prev = (seed + vals[i]) / n; out[i] = prev; continue; }32 prev = vals[i] * k + prev * (1 - k);33 out[i] = prev;34 }35 return out;36 }3738 function bollinger(bars, n, mult) {39 n = n || 20; mult = mult || 2;40 const c = closes(bars);41 const mid = sma(c, n);42 const up = new Array(c.length).fill(null);43 const lo = new Array(c.length).fill(null);44 let sum = 0, sumSq = 0;45 for (let i = 0; i < c.length; i++) {46 sum += c[i]; sumSq += c[i] * c[i];47 if (i >= n) { sum -= c[i - n]; sumSq -= c[i - n] * c[i - n]; }48 if (i >= n - 1) {49 const mean = sum / n;50 const sd = Math.sqrt(Math.max(0, sumSq / n - mean * mean));51 up[i] = mean + mult * sd;52 lo[i] = mean - mult * sd;53 }54 }55 return { mid, up, lo };56 }5758 function rsi(bars, n) {59 n = n || 14;60 const c = closes(bars);61 const out = new Array(c.length).fill(null);62 let avgG = 0, avgL = 0;63 for (let i = 1; i < c.length; i++) {64 const d = c[i] - c[i - 1];65 const g = d > 0 ? d : 0, l = d < 0 ? -d : 0;66 if (i <= n) {67 avgG += g / n; avgL += l / n;68 if (i === n) out[i] = avgL === 0 ? 100 : 100 - 100 / (1 + avgG / avgL);69 } else {70 avgG = (avgG * (n - 1) + g) / n;71 avgL = (avgL * (n - 1) + l) / n;72 out[i] = avgL === 0 ? 100 : 100 - 100 / (1 + avgG / avgL);73 }74 }75 return out;76 }7778 function macd(bars, fast, slow, signal) {79 fast = fast || 12; slow = slow || 26; signal = signal || 9;80 const c = closes(bars);81 const ef = ema(c, fast), es = ema(c, slow);82 const line = c.map((_, i) => (ef[i] != null && es[i] != null) ? ef[i] - es[i] : null);83 // EMA du signal sur la portion définie84 const start = line.findIndex(v => v != null);85 const sig = new Array(c.length).fill(null);86 if (start >= 0) {87 const seg = ema(line.slice(start), signal);88 for (let i = 0; i < seg.length; i++) sig[start + i] = seg[i];89 }90 const hist = line.map((v, i) => (v != null && sig[i] != null) ? v - sig[i] : null);91 return { line, signal: sig, hist };92 }9394 function atr(bars, n) {95 n = n || 14;96 const out = new Array(bars.length).fill(null);97 let prev = null;98 for (let i = 1; i < bars.length; i++) {99 const b = bars[i], p = bars[i - 1];100 const tr = Math.max(b.h - b.l, Math.abs(b.h - p.c), Math.abs(b.l - p.c));101 if (i <= n) {102 prev = (prev || 0) + tr / n;103 if (i === n) out[i] = prev;104 } else {105 prev = (prev * (n - 1) + tr) / n;106 out[i] = prev;107 }108 }109 return out;110 }111112 /* VWAP de session (reset à chaque journée) + bandes ±1σ / ±2σ.113 * Nécessite du volume — désactivé pour les indices. */114 function vwapSession(bars) {115 const v = new Array(bars.length).fill(null);116 const up1 = v.slice(), dn1 = v.slice(), up2 = v.slice(), dn2 = v.slice();117 let day = null, cumV = 0, cumPV = 0, cumP2V = 0;118 for (let i = 0; i < bars.length; i++) {119 const b = bars[i];120 const d = Math.floor(b.t / 86400);121 if (d !== day) { day = d; cumV = 0; cumPV = 0; cumP2V = 0; }122 const tp = (b.h + b.l + b.c) / 3;123 cumV += b.v; cumPV += tp * b.v; cumP2V += tp * tp * b.v;124 if (cumV > 0) {125 const w = cumPV / cumV;126 const sd = Math.sqrt(Math.max(0, cumP2V / cumV - w * w));127 v[i] = w; up1[i] = w + sd; dn1[i] = w - sd; up2[i] = w + 2 * sd; dn2[i] = w - 2 * sd;128 }129 }130 return { v, up1, dn1, up2, dn2 };131 }132133 /* Profil de volume sur une plage d'items visibles.134 * Retourne {rows:[{p0,p1,up,dn,total}], max, poc} — poc = index de la ligne max. */135 function volumeProfile(bars, i0, i1, nRows) {136 nRows = nRows || 24;137 let lo = Infinity, hi = -Infinity;138 for (let i = i0; i <= i1; i++) { const b = bars[i]; if (!b) continue; lo = Math.min(lo, b.l); hi = Math.max(hi, b.h); }139 if (!(hi > lo)) return null;140 const rows = Array.from({ length: nRows }, (_, k) => ({141 p0: lo + (k / nRows) * (hi - lo), p1: lo + ((k + 1) / nRows) * (hi - lo), up: 0, dn: 0, total: 0,142 }));143 const step = (hi - lo) / nRows;144 for (let i = i0; i <= i1; i++) {145 const b = bars[i];146 if (!b || !(b.v > 0)) continue;147 // volume réparti uniformément sur la plage h..l de la barre148 const span = Math.max(b.h - b.l, step * 0.5);149 const k0 = Math.max(0, Math.floor((b.l - lo) / step));150 const k1 = Math.min(nRows - 1, Math.floor((b.h - lo) / step));151 const per = b.v / (k1 - k0 + 1);152 const isUp = b.c >= b.o;153 for (let k = k0; k <= k1; k++) {154 rows[k].total += per;155 if (isUp) rows[k].up += per; else rows[k].dn += per;156 }157 }158 let max = 0, poc = 0;159 rows.forEach((r, k) => { if (r.total > max) { max = r.total; poc = k; } });160 return { rows, max, poc };161 }162163 HF.ind = { sma, ema, bollinger, rsi, macd, atr, vwapSession, volumeProfile, closes };164})();165