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 — section « v2 additions » (rendu pixel physique, 60 indicateurs + listIndicators, 42 outils + API/événements de dessin, marqueurs/lignes de prix/sessions/replay/layout/export/accessibilité), README moteur v2

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 18 days ago (Sep 7, 2026) parent 618af1e

2 changed files +161 −33

modified hfmarketdata/web/src/charts/CONTRACT.md +101 −0
@@ -143,3 +143,104 @@ Chart = {
143 143 la légende HTML porte toujours le nom + la valeur : jamais la couleur seule)
144 144 - up/down = tokens CSS `--up`/`--down` du site (sombre #2fbf71/#e5484d, clair #15803d/#d92d20) ; ΔE deutan 7,8 → encodage
145 145 secondaire obligatoire : type `hollow` disponible + préréglage daltonien (bleu #3987e5 / orange #d95926) côté page.
146 +
147 +## v2 additions (2026-09-07) — tout est ADDITIF, rien de la v1 ne change
148 +
149 +### Rendu (« sharp »)
150 +- Tout trait fin fait **1 pixel physique** (grille, mèches, contours de bougies, crosshair, axes, dernier prix, séparateurs) : à DPR 2 un trait
151 + de 0,5 px CSS centré sur `(Math.round(v·dpr)+0.5)/dpr`. Corps de bougies snappés au pixel physique, largeur physique impaire, gap ≥ 1 px
152 + physique, contour 1 px physique légèrement plus sombre ; lignes d'indicateurs **1,5 px physiques** par défaut (`width` des plots en px
153 + physiques ; `lineWidth` par instance) ; texte des axes sur pixels physiques entiers (`textBaseline:'alphabetic'` calé), police mono 11 px
154 + (chiffres tabulaires), `axisText` plus contrasté que la grille, pilules d'axe à coins 2 px, texte centré.
155 +- Testé à DPR 1, 2 et 3 (`npm run charts:shots -- --sharp=<tag> --dprs=1,2,3` → `/tmp/hfmd-charts/sharp-<tag>[-dprN][-zoom].png`).
156 +
157 +### Indicateurs (60 types ; `listIndicators()` pour la bibliothèque UI)
158 +```ts
159 +import { listIndicators, INDICATOR_CATEGORIES, INDICATOR_SOURCES } from './engine/index.js'
160 +listIndicators(): Array<{ id, name, category: 'trend'|'momentum'|'volatility'|'volume'|'levels', pane: 'main'|'new', defaults,
161 + inputs: Array<{ name, label, type: 'int'|'float'|'source'|'bool'|'select'|'time', min?, max?, step?, options? }>,
162 + plots: Array<{ kind: 'line'|'histogram'|'columns'|'band'|'cloud'|'circles'|'arrows'|'hlevels'|'profile', key?, color, width?, style?: 'solid'|'dashed', dash?, step?, connectGaps? }>,
163 + levels: number[], zones: [lo, hi][], range: {lo, hi} | null, precision: number | null, format: 'price'|'compact', dynamic: boolean, title }>
164 +// Sources : 'close' | 'open' | 'high' | 'low' | 'hl2' | 'hlc3' | 'ohlc4' (param `source` des indicateurs qui l'acceptent).
165 +```
166 +Nouveaux `IndicatorType` : overlays `hma, dema, tema, kama, alma, lsma` (+ canal de régression : upper/lower), `envelope, avwap`
167 +(`{anchor: ms|null, bands}`), `psar, zigzag` (`{deviation, mode:'percent'|'atr', atrLength, atrMult}`), `pivots`
168 +(`{type:'classic'|'fibonacci'|'camarilla'|'woodie', period:'day'|'week'|'month'}`, niveaux par session calculés depuis les barres),
169 +`volume-profile` (dynamique : recalculé sur la plage visible, histogramme horizontal à droite + POC/VAH/VAL ; `values = {rows, poc, vah, val,
170 +total}`), `auto-fib` (dynamique ; `values = {levels:[{ratio, price}], start, end, up}`). `vwap` accepte `bands` (σ) → clés `upper`/`lower`.
171 +Oscillateurs : `stochrsi, williams, roc, momentum, ao, trix, ultimate, vortex, coppock, dpo, kst, tsi, rvi, cmo, aroon, choppiness, mass,
172 +elder-ray, bb-pctb, bb-width, hv, stddev`. Volume : `ad, cmf, chaikin-osc, force, eom, klinger, volume-osc`.
173 +Les valeurs d'un indicateur dynamique ne sont pas des tableaux : `crosshairMove.indicators[id]` et `getIndicators().values` n'exposent que
174 +les clés tableau ; tester `Array.isArray`.
175 +
176 +```ts
177 +addIndicator(spec: {
178 + type, params?, colors?, id?,
179 + pane?: 'main' | 'new' | <paneId>, // v2 : un id de panneau existant → l'indicateur rejoint ce panneau (titre « A · B »)
180 + lineWidth?: number, // v2 : px physiques, toutes les lignes de l'instance
181 + styles?: Record<key, { color?, width?, style?: 'solid'|'dashed', dash?, visible?: boolean }>, // v2 : par clé tracée
182 + scale?: 'right' | 'left' | 'hidden', // v2 : overlay avec sa propre échelle (auto-range indépendant) ; 'left' affiche un axe gauche
183 +}): string // scale 'left'|'hidden' force pane 'main'
184 +setIndicatorStyle(id, { lineWidth?, styles?, colors?, scale?, visible? }): void // v2, sans recalcul
185 +getIndicators()[i].values // inchangé ; + les clés non-tableau des indicateurs dynamiques
186 +```
187 +Interaction v2 : drag sur l'axe gauche = étirement des échelles « left », double-clic = retour auto, molette sur l'axe gauche = zoom vertical.
188 +`visibleRangeChange` est inchangé ; les indicateurs `dynamic` sont recalculés par le moteur à chaque changement de plage.
189 +
190 +### Dessins (42 outils)
191 +```ts
192 +// Nouveaux outils (DRAWING_TOOLS_V2, libellés dans DRAWING_TOOL_LABELS, points dans DRAWING_POINT_COUNT) :
193 +'fib-extension'(3) 'fib-timezones'(2) 'fib-fan'(2) 'fib-arcs'(2, approximation en pixels) 'gann-fan'(2) 'gann-box'(2)
194 +'pitchfork'(3) 'schiff'(3) 'mschiff'(3) 'regression'(2 : canal de régression ±2σ sur les clôtures entre les deux ancres ; style.mult)
195 +'long'(3) 'short'(3) // points = [entrée {t0, prix}, cible {t1, prix}, stop {t1, prix}] ; créés d'UN clic (cible +2 %, stop −1 %, 20 barres),
196 + // poignées cible/stop redimensionnent, style.qty → montants ; affiche cible/stop en % et points, R:R
197 +'price-range'(2) 'date-range'(2) 'date-price-range'(2) 'vrange'(2) 'hray'(1) 'cross'(1)
198 +'path'(N : clic par point, fin par double-clic / clic sur le dernier point / Entrée / finishDrawing()) 'ellipse'(2) 'triangle'(3)
199 +'arrow-up'(1) 'arrow-down'(1) 'callout'(2 : ancre + boîte, texte multi-ligne) 'price-label'(1) 'flag'(1)
200 +'elliott-impulse'(6 : 0-5) 'elliott-correction'(4 : 0-A-B-C) 'xabcd'(5, ratios AB/XA BC/AB CD/BC AD/XA) 'head-shoulders'(7, ligne de cou par les points 2 et 4)
201 +
202 +DrawingJSON (additif) = { id, type, points, style?, text?, locked?,
203 + pane?: <paneId>, // absent = panneau principal ; outils autorisés sur un panneau d'indicateur : hline, rect, trendline, ray,
204 + // extended, vline, text, arrow, hray, cross, brush, path, ellipse, triangle, channel, measure, *-range
205 + visible?: false, timeframes?: string[], // visibilité par timeframe (absent = tous)
206 + props?: object }
207 +DrawingStyle = { color?, width?, dash?: number[] | null, fill?: 0..1, text?, fontSize?, textColor?, extendLeft?, extendRight?,
208 + showLabels?, qty?, levels?: number[] (fib*), mult? (regression) }
209 +
210 +// Méthodes v2
211 +getDrawing(id): DrawingJSON | null removeDrawing(id)
212 +getDrawingStyle(id): DrawingStyle & { text, locked, visible, timeframes, pane }
213 +setDrawingStyle(id, style: Partial<DrawingStyle> & { text?, locked?, visible?, timeframes? }) // dash accepte aussi 'dashed' | 'dotted' | null
214 +setDrawingText(id, text) // réponse attendue à l'événement textEdit
215 +setDrawingLocked(id, bool) setDrawingVisible(id, bool) setDrawingTimeframes(id, string[] | null)
216 +selectDrawing(id | id[] | null) getSelectedDrawings(): string[]
217 +duplicateDrawing(id, bars = 5): string copyDrawings(): DrawingJSON[] | null pasteDrawings(json?, bars = 5): string[]
218 +finishDrawing(): boolean nudgeDrawings(dxBars, dyTicks): boolean
219 +
220 +// Événements v2
221 +on('drawingSelect', (id: string | null, ids: string[]) => void) // pour la barre de propriétés flottante
222 +on('textEdit', ({ id, text, x, y, pane }) => void) // x, y en px du conteneur du chart ; la page affiche un input puis appelle setDrawingText
223 +on('replayChange', ({ active, index, total, bar }) => void)
224 +
225 +// Interactions v2 : Shift+clic = sélection multiple ; Alt+drag = dupliquer ; ←→ = déplacer d'une barre, ↑↓ = d'un tick (Shift = ×10) quand un
226 +// dessin est sélectionné ; Entrée termine un 'path' ; ⌘/Ctrl+C / V copier-coller, ⌘D dupliquer, ⌘L verrouiller, ⌘A tout sélectionner ;
227 +// double-clic sur un dessin à texte → textEdit ; aimantation OHLC en mode 'magnet' (panneau principal).
228 +```
229 +
230 +### Chart — fonctions additives
231 +```ts
232 +setMarkers(list: Array<{ t, position: 'above'|'below', shape?: 'arrowUp'|'arrowDown'|'circle'|'square', color?, text?, textColor?, size? }>)
233 +getMarkers()
234 +addPriceLine({ id?, price, color?, title?, style?: 'solid'|'dashed'|'dotted', width?, axisLabel?: boolean }): string
235 +updatePriceLine(id, patch) removePriceLine(id) getPriceLines()
236 +setSessions({ regular: [startHour, endHour], shade: boolean } | null) // heures murales décimales (ex. [9.5, 16]) ; ombre pré/post-marché en intraday
237 +setReplay({ index } | number | null) stepReplay(±n) getReplay(): { active, index, total, bar }
238 + // les barres après `index` sont masquées : rendu, axe, indicateurs, crosshair, getVisibleRange… ne voient que 0…index ; getData() reste complet
239 +setRightOffset(bars) setBarSpacing(px) setPriceScaleWidth('auto' | n) setTimeAxisVisible(bool) setGrid({ v?, h? })
240 +setBackground(color | { gradient: [top, bottom, …] } | null)
241 +toPNG({ scale?, watermark?, background?, legend?: boolean }) // legend : OHLC + titres/valeurs des indicateurs en haut à gauche
242 +toCSV(visibleOnly = false): string // time,open,high,low,close,volume + une colonne par série d'indicateur
243 +describeVisible(): string // phrase pour un aria-live (plage, min/max, dernier, variation, replay)
244 +```
245 +Exports supplémentaires de `engine/index.js` : `listIndicators, INDICATOR_CATEGORIES, INDICATOR_SOURCES, DRAWING_TOOLS_V1, DRAWING_TOOLS_V2,
246 +DRAWING_TOOL_LABELS, DRAWING_POINT_COUNT`. `Emitter.emit` transmet désormais tous les arguments (les écouteurs v1 à un argument ne changent pas).
modified hfmarketdata/web/src/charts/README.md +60 −33
@@ -37,17 +37,19 @@ charts/
37 37 │ ├─ render/overlay.js crosshair, axis labels, last price (pulse), High/Low markers, watermark, pane header
38 38 │ ├─ panes/pane.js Pane = two canvases + PriceScale + indicator ids
39 39 │ ├─ interactions/pointer.js Pointer Events: pan, kinetic, wheel zoom, axis drags, separators, pinch, long press, keyboard
40 −│ ├─ drawings/model.js tools, JSON (de)serialization, validation
41 −│ ├─ drawings/geometry.js distances, hit-testing (pure)
42 −│ ├─ drawings/manager.js tool state machine, selection/handles/move, undo-redo, rendering
43 −│ └─ export/png.js toPNG (composes layers, adds attribution)
40 +│ ├─ drawings/model.js 42 tools, JSON (de)serialization, validation (pane, visible, timeframes, style, props)
41 +│ ├─ drawings/geometry.js distances, primitive decomposition of every tool (seg/ray/line/poly/rect/ellipse/arc), hit-testing (pure)
42 +│ ├─ drawings/manager.js tool state machine (click, drag, multi-click), multi-selection, handles, Alt+drag, nudge, per-pane, undo-redo, rendering
43 +│ └─ export/png.js toPNG (composes layers, optional legend, attribution)
44 44 └─ indicators/
45 − ├─ index.js REGISTRY (defaults, pane, plots, levels, ranges) + computeIndicator
46 − ├─ util.js rolling mean / ema / rma / std / max / min, true range, source selection
47 − ├─ moving-averages.js sma, ema, wma, vwap
45 + ├─ index.js REGISTRY (60 types: defaults, typed inputs, category, plots, levels/zones, range, precision, dynamic) + listIndicators
46 + ├─ util.js rolling mean / ema / rma / std / sum / wma / linreg / max / min, true range, sources, compact helpers, day/week/month keys
47 + ├─ moving-averages.js sma, ema, wma, hma, dema, tema, kama, alma, lsma (+channel), envelope, vwap (+σ bands), avwap
48 48 ├─ bands.js bollinger, keltner, donchian, supertrend, ichimoku
49 49 ├─ oscillators.js rsi, macd, stoch, atr, adx, cci
50 − ├─ volume.js obv, mfi, volumeMa
50 + ├─ momentum.js stochRsi, williams %R, roc, momentum, awesome, trix, ultimate, vortex, coppock, dpo, kst, tsi, rvi, cmo, aroon, choppiness, mass index, elder ray, %B, bandwidth, hv, stddev
51 + ├─ volume.js obv, mfi, volumeMa, adLine, cmf, chaikinOsc, forceIndex, eom, klinger, volumeOsc
52 + ├─ overlays.js psar, zigzag, pivots (classic/fibonacci/camarilla/woodie · day/week/month), volumeProfile, autoFib (visible-range)
51 53 └─ heikin-ashi.js heikinAshi(bars) → bars
52 54 ```
53 55
@@ -56,10 +58,25 @@ charts/
56 58 * **Index-based X axis.** One bar = one step; session gaps never leave blank space. `TimeScale.x(i)` gives the
57 59 center of bar `i`; fractional indices are allowed (drawings, crosshair). `prependData` shifts `leftIndex` by the
58 60 number of bars added so the viewport does not move.
59 −* **Two canvases per pane** (`main` = grid, series, indicators, axis; `overlay` = crosshair, last price, drawings,
60 − pane header) plus two for the time axis. The crosshair only repaints overlays. All drawing happens in CSS pixels
61 − on a DPR-scaled context; 1 px strokes are snapped to `n + 0.5`, candle bodies are odd-width integers centered on a
62 − pixel so they stay razor sharp at every zoom.
61 +* **Two canvases per pane** (`main` = grid, series, indicators, axis; `overlay` = crosshair, last price, markers,
62 + price lines, drawings, pane header) plus two for the time axis. The crosshair only repaints overlays.
63 +* **Device-pixel crisp.** Drawing happens in CSS pixels on a DPR-scaled context, but every hairline (grid, wicks,
64 + candle outlines, crosshair, axes, last price, separators) is exactly one *device* pixel, centered with
65 + `crisp(v) = (round(v·dpr) + 0.5) / dpr` (`render/canvas.js`: `crisp`, `snap`, `hair`, `lw`, `alignFor`; the DPR of the
66 + layer being painted is published by `layer.clear()`). Candle bodies have an odd device-pixel width centered on the
67 + wick with ≥ 1 device pixel between bodies (`TimeScale.bodyWidthDevice(dpr)`), plus a one-device-pixel darker
68 + outline. Indicator lines default to 1.5 device px (`plots[].width` is in device px; `lw()` never goes below 0.75
69 + CSS px so DPR 3 stays readable). Axis text uses the alphabetic baseline snapped to device rows (`text.js: textY`)
70 + in a monospaced 11 px font (tabular digits). `npm run charts:shots -- --sharp=<tag> --dprs=1,2,3` produces
71 + before/after proofs with a ×3 nearest-neighbour zoom.
72 +* **Left axis / own scales.** An overlay indicator with `scale: 'left' | 'hidden'` owns a `PriceScale`
73 + (`ind.ownScale`, auto-ranged from its own values). When a left axis is shown the plot starts at `chart.plotX0`;
74 + pane contexts are translated by `plotX0`, and every pointer position handed to the chart is plot-relative
75 + (`hitRegion().x`), so the time scale never knows about the axis.
76 +* **Dynamic indicators** (`spec.dynamic`: volume profile, auto Fib) receive `{ from, to }` and are recomputed by
77 + `_syncDynamic` whenever the visible range or the data version changes.
78 +* **Bar replay** swaps `chart.store` for a truncated `BarStore` (`chart.fullStore` keeps everything), so rendering,
79 + axes, indicators and crosshair naturally see only bars `0…index`.
63 80 * **One draw per frame.** `chart.invalidate('data' | 'overlay' | 'layout')` sets dirty flags and schedules a single
64 81 `requestAnimationFrame`. The frame advances tweens (view animations), kinetic scrolling and price-scale
65 82 "breathing" (exponential approach, τ ≈ 70 ms), computes auto-ranges, and repaints only what is dirty. Animations
@@ -85,39 +102,49 @@ context menu is left to the caller.
85 102
86 103 ## Adding an indicator
87 104
88 −1. Write a pure function `(bars, params) → { key: (number|null)[] }` in `indicators/*.js`: `null` until the
89 − window is full, never an invented value; reuse `util.js` helpers. Output arrays may be longer than `bars` when a
90 − plot is shifted forward (Ichimoku).
91 −2. Register it in `indicators/index.js`: `{ label, compute, defaults, pane: 'main' | 'new', title(params),
92 − plots: [...], levels?, range?, format? }`. Plot kinds: `line` (`key`, `color` = series index or `'up'`/`'down'`,
93 − `width`, `dash`), `histogram` (`color: 'updown' | 'volume' | index`), `band` (`upper`, `lower`), `cloud` (`a`, `b`).
94 −3. Add hand-computed reference values to `indicators/indicators.test.js` (`npm run test:charts`).
105 +1. Write a pure function `(bars, params[, range]) → { key: (number|null)[] }` in `indicators/*.js`: `null` until
106 + the window is full, never an invented value; reuse `util.js` helpers. Output arrays may be longer than `bars`
107 + when a plot is shifted forward (Ichimoku). Dynamic indicators return plain objects computed on `range`.
108 +2. Register it in `indicators/index.js`: `{ label, category, compute, defaults, inputs: [I.int('length'), I.source()…],
109 + pane: 'main' | 'new', title(params), plots: [...], levels?, zones?, range?, format?, precision?, dynamic? }`.
110 + Plot kinds: `line` (`key`, `color` = series index or `'up'`/`'down'`, `width` in device px, `dash`/`style`,
111 + `step`, `connectGaps`), `histogram` (`color: 'updown' | 'delta' | 'volume' | 'up' | 'down' | index`), `band`
112 + (`upper`, `lower`), `cloud` (`a`, `b`), `circles` (`key`, `radius`), `arrows` (±1 values), `hlevels`
113 + (`values.levels = [{ ratio, price }]`), `profile` (volume profile object).
114 +3. Add hand-computed reference values to `indicators/indicators-v2.test.js` (`npm run test:charts`).
95 115
96 116 ## Adding a drawing tool
97 117
98 −1. Add the name to `TOOLS` and its anchor count to `POINT_COUNT` in `drawings/model.js`.
99 −2. Hit-testing: add a case to `hitDrawing` in `drawings/geometry.js` (pixel space, tolerance 6 px).
100 −3. Rendering: add a case to `DrawingManager._drawOne` in `drawings/manager.js` (points are already converted to
101 − pixels; clip to the plot is applied; use `theme.drawing` unless `style.color` is set).
102 −4. The state machine (click-click or drag to create, handles, move, undo/redo, JSON) is generic — nothing else
103 − to do. Add a serialization / hit test to `engine/drawings.test.js`.
118 +1. Add the name to `TOOLS_V2`, its anchor count to `POINT_COUNT` and a label to `TOOL_LABELS` in
119 + `drawings/model.js` (`CREATE_CLICKS` when creation needs fewer clicks than points, `MULTI_MODE` for free-form,
120 + `TEXT_TOOLS` / `PANE_TOOLS` when applicable).
121 +2. Geometry: add a case to `primitives()` in `drawings/geometry.js` returning segments / rays / lines / polys /
122 + rects / ellipses / arcs in pixel space — hit-testing (`hitDrawing`) and the line rendering come for free.
123 +3. Decorations (labels, boxes, markers): add a case to `DrawingManager._decorate` in `drawings/manager.js`.
124 +4. The state machine (creation, multi-selection, handles, move, Alt+drag, nudge, undo/redo, JSON, per-pane) is
125 + generic. Add primitive / hit tests to `engine/drawings.test.js`.
104 126
105 127 ## Tests, harness, screenshots
106 128
107 −* `npm run test:charts` — `node --test` (no DOM): scales, ticks, formats, store, indicators vs hand-computed values,
108 − drawings serialization and geometry, prepend-without-jump.
129 +* `npm run test:charts` — `node --test` (no DOM, 64 tests): scales, ticks, formats, store, 60 indicators vs
130 + hand-computed / closed-form values, registry metadata, drawings serialization, primitives and hit-tests.
109 131 * `npm run dev` then open `/dev/charts-harness.html` — synthetic data (sessions 09:30–16:00 ET, overnight and
110 132 weekend gaps, 1min / 1day, up to 200 000 bars), every series type, indicator, tool and theme, live simulation,
111 133 history prepend on `needMoreLeft`, `Bench` button. URL params are listed at the top of `dev/charts-harness.js`.
112 −* `npm run charts:shots` — Playwright: desktop 1440×900 and mobile 390×844 captures in `/tmp/hfmd-charts/`, an
134 +* `npm run charts:shots` — Playwright: desktop 1440×900 and mobile 390×844 captures in `/tmp/hfmd-charts/`
135 + (including `desktop-v2-*`: overlays, oscillators, left scale, two drawing showcases, chart features), an
113 136 interaction smoke test (drag, kinetic, wheel, axis drag, keyboard, drawing, pinch, prepend, toPNG, destroy) and
114 − the render time of 100 programmatic pan/zoom frames.
137 + the render time of 100 programmatic pan/zoom frames. `--sharp=<tag> --dprs=1,2,3` writes the sharpness proofs.
138 + Harness URL params: `ind=sma:50,obv@left,rsi,stoch@pane`, `drawings=1|2|3`, `features=1`.
115 139
116 140 ## Known limits
117 141
118 142 * `minBarSpacing` defaults to 0.5 px (contract), i.e. at most ~2 bars per pixel; pass a smaller value to show
119 143 hundreds of thousands of bars at once (decimated rendering handles it).
120 −* Text drawings are created with the placeholder `"Text"`: the engine never opens inputs — edit via
121 − `setDrawings`. Drawings live on the main pane only.
122 −* Indicator params are validated loosely (positive integers fall back to defaults); no per-plot style overrides
123 − yet beyond `colors`.
144 +* The engine never opens inputs: text / callout creation and double-clicks emit `textEdit` and the page answers
145 + with `setDrawingText`. Fib arcs are circles in pixel space (they deform when the scale is stretched).
146 +* Indicator params are validated loosely (positive integers fall back to defaults). Pivot points use the previous
147 + period's H/L/C (Woodie: current open); Klinger uses the signed-volume form; Volume Profile spreads each bar's
148 + volume uniformly over its high–low range.
149 +* A left axis is drawn for the first visible `scale: 'left'` indicator only (others still own their scale).
150 +* Replay hides bars from rendering and indicators but drawings keep their absolute times.
124 151