SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

charts: contrat du moteur de charts (API, thème, exigences de rendu)

Simon-Pierre Boucher committed 18 days ago (Sep 7, 2026) parent d25c9a4

1 changed file +145 −0

added hfmarketdata/web/src/charts/CONTRACT.md +145 −0
@@ -0,0 +1,145 @@
1 +# Contrat moteur de charts hfmarketdata — `hfmarketdata/web/src/charts/engine/index.js`
2 +
3 +Moteur de graphiques financiers **écrit de zéro** (Canvas 2D, ES modules, ZÉRO dépendance : pas de lightweight-charts,
4 +pas de d3, pas de code TradingView ni de copie d'un projet existant). Le fichier `index.js` exporte `createChart`.
5 +Tout le code, les noms, la doc et les commentaires sont en anglais (produit anglais) ; commits en français.
6 +
7 +## Données
8 +
9 +```ts
10 +type Bar = { t: number; o: number; h: number; l: number; c: number; v?: number; oi?: number }
11 +// t = millisecondes. Les timestamps de l'API sont des heures murales US/Eastern naïves ("2017-07-11 09:30:00")
12 +// ou des dates ("2024-01-02") : l'appelant les convertit avec Date.UTC(y, m-1, d, H, M, S) et le moteur formate
13 +// TOUJOURS avec les getters UTC (getUTCHours…) → les libellés affichent l'heure murale ET sans conversion.
14 +// Les barres sont triées par t croissant, sans doublon. L'axe X est INDEXÉ (une barre = un pas), pas linéaire en
15 +// temps : les trous de session (nuit, week-end) ne laissent pas de blanc, comme sur les terminaux pros.
16 +```
17 +
18 +## API
19 +
20 +```ts
21 +createChart(container: HTMLElement, options?: ChartOptions): Chart
22 +
23 +ChartOptions = {
24 + theme: Theme,
25 + timeframe: '1min' | '5min' | '30min' | '1hour' | '1day',
26 + priceFormat?: { decimals?: number | 'auto', minMove?: number }, // 'auto' = déduit des données
27 + locale?: string, // Intl pour les nombres (défaut 'en-US')
28 + sessionLabel?: string, // ex. 'ET' affiché à côté de l'heure dans l'axe/tooltip
29 + watermark?: string, // texte discret centré (ex. "AAPL · 1D · HF Market Data")
30 + rightOffsetBars?: number, // espace vide à droite (défaut 8)
31 + barSpacing?: number, // px par barre au départ (défaut 8)
32 + minBarSpacing?: number, // défaut 0.5 (rendu "ligne" automatique sous 2 px)
33 + maxBars?: number, // garde mémoire (défaut 500 000)
34 + reducedMotion?: boolean, // désactive les animations
35 +}
36 +
37 +Theme = {
38 + bg: string, paneBorder: string, grid: string, gridStrong: string,
39 + axisText: string, axisLine: string, crosshair: string, crosshairLabelBg: string, crosshairLabelText: string,
40 + up: string, down: string, upWick?: string, downWick?: string, neutral: string,
41 + volumeUp: string, volumeDown: string, // avec alpha
42 + series: string[], // 8 couleurs catégorielles validées (indicateurs, comparaisons), ordre fixe
43 + text: string, textMuted: string, accent: string,
44 + lastPriceUp: string, lastPriceDown: string,
45 + font: string, mono: string, // familles CSS ; tailles gérées par le moteur (11/12 px)
46 + drawing: string, drawingHandle: string, selection: string,
47 +}
48 +
49 +Chart = {
50 + // données
51 + setData(bars: Bar[]): void // remplace ; premier chargement → fitContent() sur les ~150 dernières barres
52 + prependData(older: Bar[]): void // historique plus ancien : le viewport reste VISUELLEMENT fixe (aucun saut)
53 + appendData(newer: Bar[]): void // barres plus récentes ; si la vue collait au bord droit, elle suit
54 + updateLast(bar: Bar): void // met à jour la dernière barre (même t) ou l'ajoute
55 + getData(): Bar[]
56 + setTimeframe(tf): void // formatage des libellés uniquement
57 +
58 + // apparence
59 + setSeriesType(type: 'candles' | 'hollow' | 'ohlc' | 'line' | 'area' | 'baseline' | 'heikin' | 'columns' | 'hlc'): void
60 + setPriceScale(o: { mode?: 'linear' | 'log' | 'percent', auto?: boolean, invert?: boolean }): void
61 + setVolume(visible: boolean): void // histogramme de volume dans le panneau principal (bas, 20 % de la hauteur) coloré up/down
62 + setTheme(theme: Theme): void
63 + setCrosshair(o: { mode?: 'normal' | 'magnet' | 'hidden', showLabels?: boolean }): void
64 + setOptions(partial: Partial<ChartOptions>): void
65 +
66 + // indicateurs (calculs dans src/charts/indicators/*.js, purs, testés)
67 + addIndicator(spec: { type: IndicatorType, params?: object, pane?: 'main' | 'new', colors?: string[], id?: string }): string
68 + updateIndicator(id: string, params: object): void
69 + removeIndicator(id: string): void
70 + getIndicators(): Array<{ id, type, params, pane, colors, values: Record<string, (number|null)[]> }>
71 + // IndicatorType : 'sma' | 'ema' | 'wma' | 'vwap' | 'bollinger' | 'keltner' | 'donchian' | 'supertrend' | 'ichimoku'
72 + // (overlay, pane 'main') · 'rsi' | 'macd' | 'stoch' | 'atr' | 'obv' | 'adx' | 'cci' | 'mfi' | 'volume-ma' (pane 'new' par défaut)
73 + // Chaque type a des params par défaut documentés (sma {length:20}, macd {fast:12,slow:26,signal:9}, rsi {length:14}…)
74 +
75 + // comparaison (overlay normalisé en % depuis la première barre visible commune ; force mode 'percent' tant qu'il y a une comparaison)
76 + addCompare(id: string, label: string, bars: Bar[], color?: string): void
77 + removeCompare(id: string): void
78 +
79 + // dessins (persistants via JSON, coordonnées en {t, price})
80 + setDrawingTool(tool: null | 'trendline' | 'ray' | 'extended' | 'hline' | 'vline' | 'rect' | 'fib' | 'measure' | 'text' | 'arrow' | 'channel' | 'brush'): void
81 + getDrawings(): DrawingJSON[]
82 + setDrawings(list: DrawingJSON[]): void
83 + clearDrawings(): void
84 + deleteSelectedDrawing(): void
85 + undo(): void; redo(): void
86 + // DrawingJSON = { id, type, points: {t:number, price:number}[], style?: {color, width, dash}, text?: string, locked?: boolean }
87 + // sélection au clic, poignées de redimensionnement, déplacement, Suppr/Backspace supprime, Échap annule l'outil,
88 + // magnétisme sur OHLC quand crosshair en mode 'magnet'. 'measure' affiche Δprix, Δ%, nb de barres, durée.
89 + // 'fib' : niveaux 0, 0.236, 0.382, 0.5, 0.618, 0.786, 1 (+1.618 en option), libellés prix.
90 +
91 + // navigation
92 + setVisibleRange(r: { fromIndex: number, toIndex: number } | { fromT: number, toT: number }, animate?: boolean): void
93 + getVisibleRange(): { fromIndex: number, toIndex: number, fromT: number | null, toT: number | null }
94 + fitContent(animate?: boolean): void
95 + scrollToLatest(animate?: boolean): void
96 + zoom(factor: number, anchorX?: number): void // >1 zoom in
97 + resetView(): void
98 + resize(): void // aussi automatique via ResizeObserver
99 + toPNG(o?: { scale?: number, watermark?: string, background?: string }): Promise<Blob>
100 + destroy(): void
101 +
102 + // événements → () => void (désabonnement)
103 + on('visibleRangeChange', (r: { fromIndex, toIndex, barsLeftOfViewport, needMoreLeft: boolean }) => void)
104 + // needMoreLeft = true quand moins de 200 barres restent hors écran à gauche (l'appelant charge l'historique)
105 + on('crosshairMove', (info: null | { index, bar, x, y, price, pane, indicators: Record<id, Record<key, number|null>>, compares: Record<id, number|null> }) => void)
106 + on('drawingsChange', (list: DrawingJSON[]) => void)
107 + on('priceScaleChange', (o) => void)
108 + on('click', (info: { index, bar, price, pane }) => void)
109 + on('toolChange', (tool) => void)
110 +}
111 +```
112 +
113 +## Exigences de rendu et d'interaction (niveau terminal professionnel)
114 +
115 +- Canvas 2D avec `devicePixelRatio` (traits 1 px nets alignés sur 0,5 ; corps de bougie ≥ 1 px ; mèche centrée sur le pixel).
116 +- Deux canvas superposés par panneau : couche principale (données, indicateurs, grille, axes) et couche overlay
117 + (crosshair, dessins, sélection) → le crosshair ne redessine jamais les données. Batching `requestAnimationFrame`, un
118 + seul draw par frame. 200 000 barres : pan/zoom à 60 fps (culler au viewport, `Path2D` par couleur, pas d'objets par barre).
119 +- Panneaux empilés (principal + un par indicateur en 'new'), séparateurs déplaçables, bouton fermer/réduire par
120 + panneau (dessiné dans le canvas ou via callback `onPaneClose`), axe des prix à DROITE pour chaque panneau, axe du temps
121 + en bas unique.
122 +- Axe des prix : ticks "nice" (1-2-5), format adapté (décimales auto, milliers), mode log, mode %, drag vertical sur
123 + l'axe = étirement de l'échelle (désactive auto), double-clic = retour auto, molette sur l'axe = zoom vertical.
124 +- Axe du temps : libellés sans collision, hiérarchisés (changement de jour en gras / de mois / d'année), format selon le
125 + timeframe ; drag horizontal sur l'axe = zoom ; libellé du crosshair avec date+heure complète.
126 +- Interaction : molette = zoom autour du curseur, drag = pan, inertie (kinetic scroll), double-clic = fitContent,
127 + clic droit géré par l'appelant (`preventDefault` non forcé), pinch-to-zoom et pan tactile, appui long = crosshair
128 + tactile, clavier (← → pan, + − zoom, Home/End, Échap), tout via Pointer Events.
129 +- Crosshair : lignes pointillées fines, étiquettes prix/temps sur les axes, mode magnet (aimante O/H/L/C), tooltip
130 + NON dessiné par le moteur (l'appelant rend une légende HTML depuis `crosshairMove`, pour l'accessibilité).
131 +- Dernier prix : ligne pointillée + étiquette colorée up/down sur l'axe ; marqueurs High/Low de la plage visible (petites
132 + étiquettes) ; grille discrète (`grid`), séparateurs de session (ligne verticale très légère au changement de jour en intraday).
133 +- Types de séries : candles pleines, hollow (creuses à la hausse = encodage secondaire daltonien), OHLC bars, line,
134 + area (dégradé), baseline (au-dessus/au-dessous d'une valeur), Heikin-Ashi, columns (close), HLC.
135 +- Animations 150–200 ms (easeOutCubic) sur fitContent/zoom programmatique/auto-scale, désactivées si `reducedMotion`.
136 +- Filigrane discret (symbole + timeframe) et attribution "hfmarketdata.io" dans `toPNG` uniquement.
137 +- Aucune fuite : `destroy()` retire listeners, ResizeObserver, rAF.
138 +
139 +## Palette validée (`scripts/validate_palette.js` du guide dataviz, surfaces #10131a / #ffffff)
140 +
141 +- series sombre : `#3987e5, #d95926, #199e70, #c98500, #d55181, #008300, #9085e9, #e66767`
142 +- series clair : `#2a78d6, #eb6834, #1baf7a, #eda100, #e87ba4, #008300, #4a3aa7, #e34948` (aqua/jaune/magenta < 3:1 →
143 + la légende HTML porte toujours le nom + la valeur : jamais la couleur seule)
144 +- up/down = tokens CSS `--up`/`--down` du site (sombre #2fbf71/#e5484d, clair #15803d/#d92d20) ; ΔE deutan 7,8 → encodage
145 + secondaire obligatoire : type `hollow` disponible + préréglage daltonien (bleu #3987e5 / orange #d95926) côté page.
146