/* ============================================================================ * HFChart — indicateurs techniques, calculés from scratch * Author : Simon-Pierre Boucher — contact@spboucher.ai * ---------------------------------------------------------------------------- * Toutes les fonctions retournent des tableaux alignés sur l'index des barres * (null avant la période de chauffe). * ==========================================================================*/ (function () { 'use strict'; const HF = (window.HF = window.HF || {}); const closes = bars => bars.map(b => b.c); function sma(vals, n) { const out = new Array(vals.length).fill(null); let sum = 0; for (let i = 0; i < vals.length; i++) { sum += vals[i]; if (i >= n) sum -= vals[i - n]; if (i >= n - 1) out[i] = sum / n; } return out; } function ema(vals, n) { const out = new Array(vals.length).fill(null); const k = 2 / (n + 1); let prev = null, seed = 0; for (let i = 0; i < vals.length; i++) { if (i < n - 1) { seed += vals[i]; continue; } if (i === n - 1) { prev = (seed + vals[i]) / n; out[i] = prev; continue; } prev = vals[i] * k + prev * (1 - k); out[i] = prev; } return out; } function bollinger(bars, n, mult) { n = n || 20; mult = mult || 2; const c = closes(bars); const mid = sma(c, n); const up = new Array(c.length).fill(null); const lo = new Array(c.length).fill(null); let sum = 0, sumSq = 0; for (let i = 0; i < c.length; i++) { sum += c[i]; sumSq += c[i] * c[i]; if (i >= n) { sum -= c[i - n]; sumSq -= c[i - n] * c[i - n]; } if (i >= n - 1) { const mean = sum / n; const sd = Math.sqrt(Math.max(0, sumSq / n - mean * mean)); up[i] = mean + mult * sd; lo[i] = mean - mult * sd; } } return { mid, up, lo }; } function rsi(bars, n) { n = n || 14; const c = closes(bars); const out = new Array(c.length).fill(null); let avgG = 0, avgL = 0; for (let i = 1; i < c.length; i++) { const d = c[i] - c[i - 1]; const g = d > 0 ? d : 0, l = d < 0 ? -d : 0; if (i <= n) { avgG += g / n; avgL += l / n; if (i === n) out[i] = avgL === 0 ? 100 : 100 - 100 / (1 + avgG / avgL); } else { avgG = (avgG * (n - 1) + g) / n; avgL = (avgL * (n - 1) + l) / n; out[i] = avgL === 0 ? 100 : 100 - 100 / (1 + avgG / avgL); } } return out; } function macd(bars, fast, slow, signal) { fast = fast || 12; slow = slow || 26; signal = signal || 9; const c = closes(bars); const ef = ema(c, fast), es = ema(c, slow); const line = c.map((_, i) => (ef[i] != null && es[i] != null) ? ef[i] - es[i] : null); // EMA du signal sur la portion définie const start = line.findIndex(v => v != null); const sig = new Array(c.length).fill(null); if (start >= 0) { const seg = ema(line.slice(start), signal); for (let i = 0; i < seg.length; i++) sig[start + i] = seg[i]; } const hist = line.map((v, i) => (v != null && sig[i] != null) ? v - sig[i] : null); return { line, signal: sig, hist }; } function atr(bars, n) { n = n || 14; const out = new Array(bars.length).fill(null); let prev = null; for (let i = 1; i < bars.length; i++) { const b = bars[i], p = bars[i - 1]; const tr = Math.max(b.h - b.l, Math.abs(b.h - p.c), Math.abs(b.l - p.c)); if (i <= n) { prev = (prev || 0) + tr / n; if (i === n) out[i] = prev; } else { prev = (prev * (n - 1) + tr) / n; out[i] = prev; } } return out; } /* VWAP de session (reset à chaque journée) + bandes ±1σ / ±2σ. * Nécessite du volume — désactivé pour les indices. */ function vwapSession(bars) { const v = new Array(bars.length).fill(null); const up1 = v.slice(), dn1 = v.slice(), up2 = v.slice(), dn2 = v.slice(); let day = null, cumV = 0, cumPV = 0, cumP2V = 0; for (let i = 0; i < bars.length; i++) { const b = bars[i]; const d = Math.floor(b.t / 86400); if (d !== day) { day = d; cumV = 0; cumPV = 0; cumP2V = 0; } const tp = (b.h + b.l + b.c) / 3; cumV += b.v; cumPV += tp * b.v; cumP2V += tp * tp * b.v; if (cumV > 0) { const w = cumPV / cumV; const sd = Math.sqrt(Math.max(0, cumP2V / cumV - w * w)); v[i] = w; up1[i] = w + sd; dn1[i] = w - sd; up2[i] = w + 2 * sd; dn2[i] = w - 2 * sd; } } return { v, up1, dn1, up2, dn2 }; } /* Profil de volume sur une plage d'items visibles. * Retourne {rows:[{p0,p1,up,dn,total}], max, poc} — poc = index de la ligne max. */ function volumeProfile(bars, i0, i1, nRows) { nRows = nRows || 24; let lo = Infinity, hi = -Infinity; 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); } if (!(hi > lo)) return null; const rows = Array.from({ length: nRows }, (_, k) => ({ p0: lo + (k / nRows) * (hi - lo), p1: lo + ((k + 1) / nRows) * (hi - lo), up: 0, dn: 0, total: 0, })); const step = (hi - lo) / nRows; for (let i = i0; i <= i1; i++) { const b = bars[i]; if (!b || !(b.v > 0)) continue; // volume réparti uniformément sur la plage h..l de la barre const span = Math.max(b.h - b.l, step * 0.5); const k0 = Math.max(0, Math.floor((b.l - lo) / step)); const k1 = Math.min(nRows - 1, Math.floor((b.h - lo) / step)); const per = b.v / (k1 - k0 + 1); const isUp = b.c >= b.o; for (let k = k0; k <= k1; k++) { rows[k].total += per; if (isUp) rows[k].up += per; else rows[k].dn += per; } } let max = 0, poc = 0; rows.forEach((r, k) => { if (r.total > max) { max = r.total; poc = k; } }); return { rows, max, poc }; } HF.ind = { sma, ema, bollinger, rsi, macd, atr, vwapSession, volumeProfile, closes }; })();