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 — transformations de séries (from scratch, zéro dépendance)3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * Un "bar" = {t,o,h,l,c,v}. Chaque transform retourne un tableau de barres6 * pseudo-temporelles ; l'axe X reste l'index de barre, comme sur tout chart HF.7 * ==========================================================================*/8(function () {9 'use strict';10 const HF = (window.HF = window.HF || {});1112 /* Arrondi "joli" (1 / 2 / 2.5 / 5 × 10^n) — sert de taille de brique auto. */13 function niceRound(x) {14 if (!(x > 0)) return 1;15 const p = Math.pow(10, Math.floor(Math.log10(x)));16 const m = x / p;17 const step = m < 1.5 ? 1 : m < 2.25 ? 2 : m < 3.5 ? 2.5 : m < 7.5 ? 5 : 10;18 return step * p;19 }2021 /* Brique auto façon ATR : moyenne des true ranges des ~500 dernières barres. */22 function autoBrick(bars, k) {23 const n = bars.length;24 if (n < 3) return niceRound((n ? bars[n - 1].c : 1) * 0.005);25 let sum = 0, cnt = 0;26 for (let i = Math.max(1, n - 500); i < n; i++) {27 const b = bars[i], p = bars[i - 1];28 sum += Math.max(b.h - b.l, Math.abs(b.h - p.c), Math.abs(b.l - p.c));29 cnt++;30 }31 return niceRound((sum / cnt) * (k || 1)) || niceRound(bars[n - 1].c * 0.002);32 }3334 function heikinAshi(bars) {35 const out = new Array(bars.length);36 let po = 0, pc = 0;37 for (let i = 0; i < bars.length; i++) {38 const b = bars[i];39 const c = (b.o + b.h + b.l + b.c) / 4;40 const o = i === 0 ? (b.o + b.c) / 2 : (po + pc) / 2;41 out[i] = { t: b.t, o, h: Math.max(b.h, o, c), l: Math.min(b.l, o, c), c, v: b.v };42 po = o; pc = c;43 }44 return out;45 }4647 /* Renko : briques de taille fixe, renversement = 2 briques. */48 function renko(bars, opts) {49 const brick = (opts && opts.brick) || autoBrick(bars, 1);50 const out = [];51 if (!bars.length || !(brick > 0)) return out;52 let lo = Math.floor(bars[0].c / brick) * brick; // plancher de la brique courante53 let hi = lo + brick;54 let dir = 0, vol = 0;55 const push = (t, up) => {56 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; }57 else { out.push({ t, o: lo, c: lo - brick, h: lo, l: lo - brick, v: vol, up: false }); hi = lo; lo -= brick; dir = -1; }58 vol = 0;59 };60 for (const b of bars) {61 vol += b.v;62 for (;;) {63 if (b.c >= hi + brick - 1e-9 && dir >= 0) push(b.t, true);64 else if (b.c <= lo - brick + 1e-9 && dir <= 0) push(b.t, false);65 else if (dir > 0 && b.c <= lo - brick + 1e-9) push(b.t, false); // renversement (2 briques sous hi)66 else if (dir < 0 && b.c >= hi + brick - 1e-9) push(b.t, true); // renversement haussier67 else break;68 }69 }70 return out;71 }7273 /* Three-Line Break : nouvelle barre seulement si le close casse les N dernières. */74 function lineBreak(bars, opts) {75 const nBreak = (opts && opts.lines) || 3;76 const out = [];77 for (const b of bars) {78 if (!out.length) {79 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 });80 continue;81 }82 const last = out[out.length - 1];83 const win = out.slice(-nBreak);84 let maxC = -Infinity, minC = Infinity;85 for (const x of win) { maxC = Math.max(maxC, x.o, x.c); minC = Math.min(minC, x.o, x.c); }86 if (b.c > maxC + 1e-9) {87 const o = Math.max(last.o, last.c);88 out.push({ t: b.t, o, c: b.c, h: b.c, l: o, v: b.v, up: true });89 } else if (b.c < minC - 1e-9) {90 const o = Math.min(last.o, last.c);91 out.push({ t: b.t, o, c: b.c, h: o, l: b.c, v: b.v, up: false });92 }93 }94 return out;95 }9697 /* Range Bars : chaque barre couvre exactement `range` de haut en bas. */98 function rangeBars(bars, opts) {99 const range = (opts && opts.range) || autoBrick(bars, 1.5);100 const out = [];101 if (!bars.length || !(range > 0)) return out;102 let cur = null;103 for (const b of bars) {104 for (const p of [b.o, b.h, b.l, b.c]) {105 if (!cur) { cur = { t: b.t, o: p, h: p, l: p, c: p, v: 0 }; continue; }106 cur.h = Math.max(cur.h, p); cur.l = Math.min(cur.l, p);107 cur.c = p; cur.t = b.t;108 if (cur.h - cur.l >= range - 1e-9) {109 const up = cur.c >= cur.o;110 const h = cur.l + range, l = cur.l;111 const hh = up ? h : cur.h, ll = up ? cur.l : cur.h - range;112 const close = up ? hh : ll;113 out.push({ t: cur.t, o: cur.o, h: hh, l: ll, c: close, v: cur.v + b.v, up });114 cur = { t: b.t, o: close, h: close, l: close, c: close, v: 0 };115 }116 }117 if (cur) cur.v += b.v;118 }119 return out;120 }121122 /* Kagi : item = une ligne verticale {o: départ, c: arrivée, up: yang (épaisse)}.123 * Passe yang au-dessus de l'épaule précédente, yin sous la taille précédente. */124 function kagi(bars, opts) {125 const rev = (opts && opts.reversal) || autoBrick(bars, 3);126 const out = [];127 if (!bars.length || !(rev > 0)) return out;128 let dir = 0, anchor = bars[0].c, extreme = bars[0].c, yang = true;129 let prevShoulder = null, prevWaist = null;130 for (const b of bars) {131 const p = b.c;132 if (dir === 0) {133 extreme = p;134 if (p >= anchor + rev) dir = 1;135 else if (p <= anchor - rev) dir = -1;136 continue;137 }138 if (dir > 0) {139 if (p > extreme) extreme = p;140 if (extreme - p >= rev) {141 if (prevShoulder != null && extreme > prevShoulder) yang = true;142 out.push({ t: b.t, o: anchor, c: extreme, h: extreme, l: anchor, v: 0, up: yang });143 prevShoulder = extreme;144 anchor = extreme; extreme = p; dir = -1;145 }146 } else {147 if (p < extreme) extreme = p;148 if (p - extreme >= rev) {149 if (prevWaist != null && extreme < prevWaist) yang = false;150 out.push({ t: b.t, o: anchor, c: extreme, h: anchor, l: extreme, v: 0, up: yang });151 prevWaist = extreme;152 anchor = extreme; extreme = p; dir = 1;153 }154 }155 }156 if (dir !== 0 && extreme !== anchor) {157 out.push({ t: bars[bars.length - 1].t, o: anchor, c: extreme,158 h: Math.max(anchor, extreme), l: Math.min(anchor, extreme), v: 0, up: yang });159 }160 return out;161 }162163 /* Point & Figure : colonnes de X (hausse) / O (baisse), renversement N boîtes.164 * item = {type:'X'|'O', lo, n, box} → n boîtes empilées à partir de lo. */165 function pnf(bars, opts) {166 const box = (opts && opts.box) || autoBrick(bars, 1);167 const revN = (opts && opts.reversal) || 3;168 const cols = [];169 if (!bars.length || !(box > 0)) return cols;170 const fb = p => Math.floor(p / box + 1e-9);171 let dir = 0, top = fb(bars[0].c), bot = top;172 const pushCol = (t, type) => {173 cols.push({174 t, type, lo: bot * box, n: top - bot + 1, box,175 o: type === 'X' ? bot * box : (top + 1) * box,176 c: type === 'X' ? (top + 1) * box : bot * box,177 h: (top + 1) * box, l: bot * box, v: 0, up: type === 'X',178 });179 };180 for (const b of bars) {181 const hiB = fb(b.h), loB = fb(b.l);182 if (dir === 0) {183 if (hiB > top) { dir = 1; top = hiB; }184 else if (loB < bot) { dir = -1; bot = loB; }185 continue;186 }187 if (dir > 0) {188 if (hiB > top) top = hiB;189 else if (top - loB >= revN) {190 pushCol(b.t, 'X');191 const newTop = top - 1;192 bot = loB; top = newTop; dir = -1;193 }194 } else {195 if (loB < bot) bot = loB;196 else if (hiB - bot >= revN) {197 pushCol(b.t, 'O');198 const newBot = bot + 1;199 top = hiB; bot = newBot; dir = 1;200 }201 }202 }203 if (dir !== 0 && top >= bot) pushCol(bars[bars.length - 1].t, dir > 0 ? 'X' : 'O');204 return cols;205 }206207 HF.transforms = { heikinAshi, renko, lineBreak, rangeBars, kagi, pnf, autoBrick, niceRound };208})();209