/* ============================================================================ * HFChart — transformations de séries (from scratch, zéro dépendance) * Author : Simon-Pierre Boucher — contact@spboucher.ai * ---------------------------------------------------------------------------- * Un "bar" = {t,o,h,l,c,v}. Chaque transform retourne un tableau de barres * pseudo-temporelles ; l'axe X reste l'index de barre, comme sur tout chart HF. * ==========================================================================*/ (function () { 'use strict'; const HF = (window.HF = window.HF || {}); /* Arrondi "joli" (1 / 2 / 2.5 / 5 × 10^n) — sert de taille de brique auto. */ function niceRound(x) { if (!(x > 0)) return 1; const p = Math.pow(10, Math.floor(Math.log10(x))); const m = x / p; const step = m < 1.5 ? 1 : m < 2.25 ? 2 : m < 3.5 ? 2.5 : m < 7.5 ? 5 : 10; return step * p; } /* Brique auto façon ATR : moyenne des true ranges des ~500 dernières barres. */ function autoBrick(bars, k) { const n = bars.length; if (n < 3) return niceRound((n ? bars[n - 1].c : 1) * 0.005); let sum = 0, cnt = 0; for (let i = Math.max(1, n - 500); i < n; i++) { const b = bars[i], p = bars[i - 1]; sum += Math.max(b.h - b.l, Math.abs(b.h - p.c), Math.abs(b.l - p.c)); cnt++; } return niceRound((sum / cnt) * (k || 1)) || niceRound(bars[n - 1].c * 0.002); } function heikinAshi(bars) { const out = new Array(bars.length); let po = 0, pc = 0; for (let i = 0; i < bars.length; i++) { const b = bars[i]; const c = (b.o + b.h + b.l + b.c) / 4; const o = i === 0 ? (b.o + b.c) / 2 : (po + pc) / 2; out[i] = { t: b.t, o, h: Math.max(b.h, o, c), l: Math.min(b.l, o, c), c, v: b.v }; po = o; pc = c; } return out; } /* Renko : briques de taille fixe, renversement = 2 briques. */ function renko(bars, opts) { const brick = (opts && opts.brick) || autoBrick(bars, 1); const out = []; if (!bars.length || !(brick > 0)) return out; let lo = Math.floor(bars[0].c / brick) * brick; // plancher de la brique courante let hi = lo + brick; let dir = 0, vol = 0; const push = (t, up) => { if (up) { out.push({ t, o: hi, c: hi + brick, h: hi + brick, l: hi, v: vol, up: true }); lo = hi; hi += brick; dir = 1; } else { out.push({ t, o: lo, c: lo - brick, h: lo, l: lo - brick, v: vol, up: false }); hi = lo; lo -= brick; dir = -1; } vol = 0; }; for (const b of bars) { vol += b.v; for (;;) { if (b.c >= hi + brick - 1e-9 && dir >= 0) push(b.t, true); else if (b.c <= lo - brick + 1e-9 && dir <= 0) push(b.t, false); else if (dir > 0 && b.c <= lo - brick + 1e-9) push(b.t, false); // renversement (2 briques sous hi) else if (dir < 0 && b.c >= hi + brick - 1e-9) push(b.t, true); // renversement haussier else break; } } return out; } /* Three-Line Break : nouvelle barre seulement si le close casse les N dernières. */ function lineBreak(bars, opts) { const nBreak = (opts && opts.lines) || 3; const out = []; for (const b of bars) { if (!out.length) { if (b.c !== b.o) out.push({ t: b.t, o: b.o, c: b.c, h: Math.max(b.o, b.c), l: Math.min(b.o, b.c), v: b.v, up: b.c >= b.o }); continue; } const last = out[out.length - 1]; const win = out.slice(-nBreak); let maxC = -Infinity, minC = Infinity; for (const x of win) { maxC = Math.max(maxC, x.o, x.c); minC = Math.min(minC, x.o, x.c); } if (b.c > maxC + 1e-9) { const o = Math.max(last.o, last.c); out.push({ t: b.t, o, c: b.c, h: b.c, l: o, v: b.v, up: true }); } else if (b.c < minC - 1e-9) { const o = Math.min(last.o, last.c); out.push({ t: b.t, o, c: b.c, h: o, l: b.c, v: b.v, up: false }); } } return out; } /* Range Bars : chaque barre couvre exactement `range` de haut en bas. */ function rangeBars(bars, opts) { const range = (opts && opts.range) || autoBrick(bars, 1.5); const out = []; if (!bars.length || !(range > 0)) return out; let cur = null; for (const b of bars) { for (const p of [b.o, b.h, b.l, b.c]) { if (!cur) { cur = { t: b.t, o: p, h: p, l: p, c: p, v: 0 }; continue; } cur.h = Math.max(cur.h, p); cur.l = Math.min(cur.l, p); cur.c = p; cur.t = b.t; if (cur.h - cur.l >= range - 1e-9) { const up = cur.c >= cur.o; const h = cur.l + range, l = cur.l; const hh = up ? h : cur.h, ll = up ? cur.l : cur.h - range; const close = up ? hh : ll; out.push({ t: cur.t, o: cur.o, h: hh, l: ll, c: close, v: cur.v + b.v, up }); cur = { t: b.t, o: close, h: close, l: close, c: close, v: 0 }; } } if (cur) cur.v += b.v; } return out; } /* Kagi : item = une ligne verticale {o: départ, c: arrivée, up: yang (épaisse)}. * Passe yang au-dessus de l'épaule précédente, yin sous la taille précédente. */ function kagi(bars, opts) { const rev = (opts && opts.reversal) || autoBrick(bars, 3); const out = []; if (!bars.length || !(rev > 0)) return out; let dir = 0, anchor = bars[0].c, extreme = bars[0].c, yang = true; let prevShoulder = null, prevWaist = null; for (const b of bars) { const p = b.c; if (dir === 0) { extreme = p; if (p >= anchor + rev) dir = 1; else if (p <= anchor - rev) dir = -1; continue; } if (dir > 0) { if (p > extreme) extreme = p; if (extreme - p >= rev) { if (prevShoulder != null && extreme > prevShoulder) yang = true; out.push({ t: b.t, o: anchor, c: extreme, h: extreme, l: anchor, v: 0, up: yang }); prevShoulder = extreme; anchor = extreme; extreme = p; dir = -1; } } else { if (p < extreme) extreme = p; if (p - extreme >= rev) { if (prevWaist != null && extreme < prevWaist) yang = false; out.push({ t: b.t, o: anchor, c: extreme, h: anchor, l: extreme, v: 0, up: yang }); prevWaist = extreme; anchor = extreme; extreme = p; dir = 1; } } } if (dir !== 0 && extreme !== anchor) { out.push({ t: bars[bars.length - 1].t, o: anchor, c: extreme, h: Math.max(anchor, extreme), l: Math.min(anchor, extreme), v: 0, up: yang }); } return out; } /* Point & Figure : colonnes de X (hausse) / O (baisse), renversement N boîtes. * item = {type:'X'|'O', lo, n, box} → n boîtes empilées à partir de lo. */ function pnf(bars, opts) { const box = (opts && opts.box) || autoBrick(bars, 1); const revN = (opts && opts.reversal) || 3; const cols = []; if (!bars.length || !(box > 0)) return cols; const fb = p => Math.floor(p / box + 1e-9); let dir = 0, top = fb(bars[0].c), bot = top; const pushCol = (t, type) => { cols.push({ t, type, lo: bot * box, n: top - bot + 1, box, o: type === 'X' ? bot * box : (top + 1) * box, c: type === 'X' ? (top + 1) * box : bot * box, h: (top + 1) * box, l: bot * box, v: 0, up: type === 'X', }); }; for (const b of bars) { const hiB = fb(b.h), loB = fb(b.l); if (dir === 0) { if (hiB > top) { dir = 1; top = hiB; } else if (loB < bot) { dir = -1; bot = loB; } continue; } if (dir > 0) { if (hiB > top) top = hiB; else if (top - loB >= revN) { pushCol(b.t, 'X'); const newTop = top - 1; bot = loB; top = newTop; dir = -1; } } else { if (loB < bot) bot = loB; else if (hiB - bot >= revN) { pushCol(b.t, 'O'); const newBot = bot + 1; top = hiB; bot = newBot; dir = 1; } } } if (dir !== 0 && top >= bot) pushCol(bars[bars.length - 1].t, dir > 0 ? 'X' : 'O'); return cols; } HF.transforms = { heikinAshi, renko, lineBreak, rangeBars, kagi, pnf, autoBrick, niceRound }; })();