@livo-build/charts 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,42 @@ export declare function macd(values: number[], fast?: number, slow?: number, sig
44
44
  signal: (number | null)[];
45
45
  hist: (number | null)[];
46
46
  };
47
+ /**
48
+ * Stochastic oscillator. `%K = 100·(close − lowestLow) / (highestHigh − lowestLow)` over
49
+ * `kPeriod` (default 14), optionally smoothed by an SMA of `smooth` (default 1 = "fast"),
50
+ * and `%D` = SMA(%K, `dPeriod`, default 3). Both bounded 0–100, aligned to input.
51
+ */
52
+ export declare function stochastic(candles: Candle[], kPeriod?: number, dPeriod?: number, smooth?: number): {
53
+ k: (number | null)[];
54
+ d: (number | null)[];
55
+ };
56
+ /**
57
+ * Average True Range (Wilder) — a volatility measure in price units. TR is the greatest of
58
+ * (high−low), |high−prevClose|, |low−prevClose|; ATR seeds with the SMA of the first
59
+ * `period` TRs (default 14) then Wilder-smooths. Aligned to input; null in the warmup.
60
+ */
61
+ export declare function atr(candles: Candle[], period?: number): (number | null)[];
62
+ /** One price level of a {@link volumeProfile} histogram. `lo`/`hi` bound the bucket. */
63
+ export interface VolumeBucket {
64
+ lo: number;
65
+ hi: number;
66
+ vol: number;
67
+ }
68
+ /** A computed volume-by-price profile: equal-height buckets plus the point of control. */
69
+ export interface VolumeProfileResult {
70
+ buckets: VolumeBucket[];
71
+ /** Largest bucket volume (for scaling the bars). */
72
+ maxVol: number;
73
+ /** Mid-price of the highest-volume bucket (the Point of Control). */
74
+ poc: number;
75
+ }
76
+ /**
77
+ * Volume-by-price histogram. Splits the candles' price range into `buckets` equal bands
78
+ * and distributes each candle's volume evenly across the bands its low→high spans (so a
79
+ * wide bar contributes to several levels). Returns the bands, the peak volume, and the
80
+ * Point of Control (mid-price of the heaviest band).
81
+ */
82
+ export declare function volumeProfile(candles: Candle[], buckets?: number): VolumeProfileResult;
47
83
  /** Pull a price series (default close) out of candles. */
48
84
  export declare function sourceValues(candles: Candle[], source?: PriceSource): number[];
49
85
  /** Compute an indicator's value series, aligned to `candles` (null where undefined). */
@@ -155,6 +155,125 @@ export function macd(values, fast = 12, slow = 26, signal = 9) {
155
155
  const hist = line.map((m, i) => (m != null && sig[i] != null ? m - sig[i] : null));
156
156
  return { macd: line, signal: sig, hist };
157
157
  }
158
+ /**
159
+ * Stochastic oscillator. `%K = 100·(close − lowestLow) / (highestHigh − lowestLow)` over
160
+ * `kPeriod` (default 14), optionally smoothed by an SMA of `smooth` (default 1 = "fast"),
161
+ * and `%D` = SMA(%K, `dPeriod`, default 3). Both bounded 0–100, aligned to input.
162
+ */
163
+ export function stochastic(candles, kPeriod = 14, dPeriod = 3, smooth = 1) {
164
+ const n = candles.length;
165
+ const rawK = new Array(n).fill(null);
166
+ if (!(kPeriod > 0))
167
+ return { k: rawK, d: rawK.slice() };
168
+ for (let i = kPeriod - 1; i < n; i++) {
169
+ let lo = Infinity;
170
+ let hi = -Infinity;
171
+ for (let j = i - kPeriod + 1; j <= i; j++) {
172
+ lo = Math.min(lo, candles[j].l);
173
+ hi = Math.max(hi, candles[j].h);
174
+ }
175
+ const rng = hi - lo;
176
+ rawK[i] = rng > 0 ? (100 * (candles[i].c - lo)) / rng : 100;
177
+ }
178
+ const k = smooth > 1 ? smaNullable(rawK, smooth) : rawK;
179
+ const d = smaNullable(k, dPeriod);
180
+ return { k, d };
181
+ }
182
+ /** SMA over a series that may contain leading nulls (used to smooth %K / %D). */
183
+ function smaNullable(series, period) {
184
+ const out = new Array(series.length).fill(null);
185
+ if (!(period > 0))
186
+ return out;
187
+ let sum = 0;
188
+ let cnt = 0;
189
+ const win = [];
190
+ for (let i = 0; i < series.length; i++) {
191
+ const v = series[i];
192
+ if (v == null) {
193
+ // a null breaks the window — restart so we never average across a gap.
194
+ sum = 0;
195
+ cnt = 0;
196
+ win.length = 0;
197
+ continue;
198
+ }
199
+ win.push(v);
200
+ sum += v;
201
+ cnt++;
202
+ if (cnt > period)
203
+ sum -= win[cnt - period - 1];
204
+ if (cnt >= period)
205
+ out[i] = sum / period;
206
+ }
207
+ return out;
208
+ }
209
+ /**
210
+ * Average True Range (Wilder) — a volatility measure in price units. TR is the greatest of
211
+ * (high−low), |high−prevClose|, |low−prevClose|; ATR seeds with the SMA of the first
212
+ * `period` TRs (default 14) then Wilder-smooths. Aligned to input; null in the warmup.
213
+ */
214
+ export function atr(candles, period = 14) {
215
+ const n = candles.length;
216
+ const out = new Array(n).fill(null);
217
+ if (!(period > 0) || n <= period)
218
+ return out;
219
+ const tr = new Array(n).fill(0);
220
+ tr[0] = candles[0].h - candles[0].l;
221
+ for (let i = 1; i < n; i++) {
222
+ const pc = candles[i - 1].c;
223
+ tr[i] = Math.max(candles[i].h - candles[i].l, Math.abs(candles[i].h - pc), Math.abs(candles[i].l - pc));
224
+ }
225
+ // seed = SMA of the first `period` TRs (placed at index `period`, like RSI's warmup).
226
+ let prev = 0;
227
+ for (let i = 1; i <= period; i++)
228
+ prev += tr[i];
229
+ prev /= period;
230
+ out[period] = prev;
231
+ for (let i = period + 1; i < n; i++) {
232
+ prev = (prev * (period - 1) + tr[i]) / period;
233
+ out[i] = prev;
234
+ }
235
+ return out;
236
+ }
237
+ /**
238
+ * Volume-by-price histogram. Splits the candles' price range into `buckets` equal bands
239
+ * and distributes each candle's volume evenly across the bands its low→high spans (so a
240
+ * wide bar contributes to several levels). Returns the bands, the peak volume, and the
241
+ * Point of Control (mid-price of the heaviest band).
242
+ */
243
+ export function volumeProfile(candles, buckets = 24) {
244
+ const B = Math.max(1, Math.round(buckets));
245
+ let lo = Infinity;
246
+ let hi = -Infinity;
247
+ for (const c of candles) {
248
+ lo = Math.min(lo, c.l);
249
+ hi = Math.max(hi, c.h);
250
+ }
251
+ if (!isFinite(lo) || !isFinite(hi) || hi <= lo) {
252
+ const v = candles.reduce((a, c) => a + (c.vol || 0), 0);
253
+ const p = isFinite(lo) ? lo : 0;
254
+ return { buckets: [{ lo: p, hi: p, vol: v }], maxVol: v, poc: p };
255
+ }
256
+ const step = (hi - lo) / B;
257
+ const bins = new Array(B).fill(0);
258
+ for (const c of candles) {
259
+ const from = Math.max(0, Math.min(B - 1, Math.floor((c.l - lo) / step)));
260
+ const to = Math.max(0, Math.min(B - 1, Math.floor((c.h - lo) / step)));
261
+ const span = to - from + 1;
262
+ const share = (c.vol || 0) / span;
263
+ for (let b = from; b <= to; b++)
264
+ bins[b] += share;
265
+ }
266
+ let maxVol = 0;
267
+ let pocIdx = 0;
268
+ const out = bins.map((vol, i) => {
269
+ if (vol > maxVol) {
270
+ maxVol = vol;
271
+ pocIdx = i;
272
+ }
273
+ return { lo: lo + i * step, hi: lo + (i + 1) * step, vol };
274
+ });
275
+ return { buckets: out, maxVol: maxVol || 1, poc: lo + (pocIdx + 0.5) * step };
276
+ }
158
277
  const SOURCE_KEY = { open: "o", high: "h", low: "l", close: "c" };
159
278
  /** Pull a price series (default close) out of candles. */
160
279
  export function sourceValues(candles, source = "close") {
@@ -7,3 +7,13 @@ export declare function transformPrice(p: number, t?: PriceTransform): number;
7
7
  * price is non-positive or non-finite are dropped; their volume is dropped with them.
8
8
  */
9
9
  export declare function buildOHLC(points: Point[], interval: number, transform?: PriceTransform): Candle[];
10
+ /**
11
+ * Heikin-Ashi transform: smoothed candles that filter noise and make trends easier to
12
+ * read. Each output keeps the input's `time` and `vol`; only OHLC are recomputed:
13
+ * HA_close = (o+h+l+c)/4
14
+ * HA_open = (prevHA_open + prevHA_close)/2 (seed: (o+c)/2)
15
+ * HA_high = max(h, HA_open, HA_close)
16
+ * HA_low = min(l, HA_open, HA_close)
17
+ * Input must be ascending by time; output is aligned 1:1.
18
+ */
19
+ export declare function heikinAshi(candles: Candle[]): Candle[];
package/dist/core/ohlc.js CHANGED
@@ -34,3 +34,33 @@ export function buildOHLC(points, interval, transform = {}) {
34
34
  }
35
35
  return [...buckets.values()].sort((a, b) => a.time - b.time);
36
36
  }
37
+ /**
38
+ * Heikin-Ashi transform: smoothed candles that filter noise and make trends easier to
39
+ * read. Each output keeps the input's `time` and `vol`; only OHLC are recomputed:
40
+ * HA_close = (o+h+l+c)/4
41
+ * HA_open = (prevHA_open + prevHA_close)/2 (seed: (o+c)/2)
42
+ * HA_high = max(h, HA_open, HA_close)
43
+ * HA_low = min(l, HA_open, HA_close)
44
+ * Input must be ascending by time; output is aligned 1:1.
45
+ */
46
+ export function heikinAshi(candles) {
47
+ const out = new Array(candles.length);
48
+ let prevO = 0;
49
+ let prevC = 0;
50
+ for (let i = 0; i < candles.length; i++) {
51
+ const c = candles[i];
52
+ const haC = (c.o + c.h + c.l + c.c) / 4;
53
+ const haO = i === 0 ? (c.o + c.c) / 2 : (prevO + prevC) / 2;
54
+ out[i] = {
55
+ time: c.time,
56
+ o: haO,
57
+ h: Math.max(c.h, haO, haC),
58
+ l: Math.min(c.l, haO, haC),
59
+ c: haC,
60
+ vol: c.vol,
61
+ };
62
+ prevO = haO;
63
+ prevC = haC;
64
+ }
65
+ return out;
66
+ }
@@ -0,0 +1,53 @@
1
+ import type { Point } from "./types";
2
+ import type { ChartFeed } from "./feed";
3
+ /** The public Polymarket CLOB host. */
4
+ export declare const POLYMARKET_CLOB = "https://clob.polymarket.com";
5
+ /** One raw `/prices-history` row: `t` is unix seconds, `p` is the price (probability 0–1). */
6
+ export interface PolyHistoryPoint {
7
+ t: number;
8
+ p: number;
9
+ }
10
+ export interface FetchPolyHistoryOptions {
11
+ /** The CLOB token id (an outcome of a market — see Polymarket's resolveMarket). */
12
+ tokenId: string;
13
+ /** Window start (unix seconds). Omit to use `interval` instead. */
14
+ startTs?: number;
15
+ /** Window end (unix seconds). Omit to use `interval` instead. */
16
+ endTs?: number;
17
+ /** Coarse window when `startTs`/`endTs` aren't given (default "1d"). */
18
+ interval?: "1m" | "1h" | "6h" | "1d" | "1w" | "max";
19
+ /** Resolution in minutes per point (e.g. 60 = hourly points). */
20
+ fidelity?: number;
21
+ /** Override the CLOB host. */
22
+ host?: string;
23
+ }
24
+ /** Fetch raw Polymarket price-history points (probabilities) for a token. */
25
+ export declare function fetchPolymarketPriceHistory(opts: FetchPolyHistoryOptions): Promise<Point[]>;
26
+ export interface PolymarketFeedOptions {
27
+ /** The CLOB token id to chart. */
28
+ tokenId: string;
29
+ /** Candle bucket size in seconds (default 3600 = 1h). Drives the price-history fidelity. */
30
+ bucketSeconds?: number;
31
+ /** Override the CLOB host. */
32
+ host?: string;
33
+ /** Live poll cadence in ms (default 30s; 0 disables the live poll). */
34
+ refetchMs?: number;
35
+ /**
36
+ * Optional Livo indexer base (e.g. "https://polymarket.livo.build"). When set (and
37
+ * EventSource is available), the live candle is driven by the indexer's `/stream` SSE
38
+ * — INSTANT updates on every price/trade — instead of the 30s history poll. History
39
+ * still loads from the public CLOB, so this is a pure realtime upgrade. Omit to keep
40
+ * the dependency-free poll-only behavior.
41
+ */
42
+ liveUrl?: string;
43
+ /** Override "now" (ms) — for tests/determinism. */
44
+ now?: number;
45
+ }
46
+ /**
47
+ * A {@link ChartFeed} for a Polymarket outcome: paged price history bucketed into OHLC
48
+ * candles, plus a "live" candle. By default the latest bucket is re-fetched on an
49
+ * interval (Polymarket has no public candle socket). Pass `liveUrl` (a Livo indexer) to
50
+ * upgrade the live candle to INSTANT SSE updates. Price-history has no volume, so
51
+ * history candles carry `vol: 0`. Pass it to `connectFeed` with `bucketSeconds`.
52
+ */
53
+ export declare function polymarketFeed(opts: PolymarketFeedOptions): ChartFeed;
@@ -0,0 +1,173 @@
1
+ // Polymarket adapter — chart any prediction-market outcome from the public CLOB
2
+ // price-history endpoint. Dependency-free (uses the global fetch); no API key needed
3
+ // for read-only price history. Prices are probabilities in [0, 1].
4
+ //
5
+ // import { Chart, connectFeed, polymarketFeed } from "@livo-build/charts";
6
+ // const chart = new Chart(el, { height: 420 });
7
+ // connectFeed(chart, polymarketFeed({ tokenId, bucketSeconds: 3600 }), { interval: 3600 });
8
+ import { buildOHLC } from "./ohlc";
9
+ /** The public Polymarket CLOB host. */
10
+ export const POLYMARKET_CLOB = "https://clob.polymarket.com";
11
+ /** fetch with an abort timeout so a stalled CLOB request can't wedge lazy-loading forever. */
12
+ async function fetchT(input, init = {}, timeoutMs = 12000) {
13
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
14
+ const timer = ac ? setTimeout(() => ac.abort(), timeoutMs) : undefined;
15
+ try {
16
+ return await fetch(input, ac ? { ...init, signal: ac.signal } : init);
17
+ }
18
+ finally {
19
+ if (timer)
20
+ clearTimeout(timer);
21
+ }
22
+ }
23
+ /** Fetch raw Polymarket price-history points (probabilities) for a token. */
24
+ export async function fetchPolymarketPriceHistory(opts) {
25
+ const host = opts.host ?? POLYMARKET_CLOB;
26
+ const p = new URLSearchParams({ market: opts.tokenId });
27
+ if (opts.startTs != null && opts.endTs != null) {
28
+ p.set("startTs", String(Math.floor(opts.startTs)));
29
+ p.set("endTs", String(Math.floor(opts.endTs)));
30
+ }
31
+ else {
32
+ p.set("interval", opts.interval ?? "1d");
33
+ }
34
+ if (opts.fidelity)
35
+ p.set("fidelity", String(Math.max(1, Math.round(opts.fidelity))));
36
+ const res = await fetchT(`${host}/prices-history?${p.toString()}`);
37
+ if (!res.ok)
38
+ throw new Error("Polymarket prices-history " + res.status);
39
+ const json = (await res.json());
40
+ return (json.history ?? []).map((h) => ({ t: h.t, p: h.p, v: 0 }));
41
+ }
42
+ /**
43
+ * A {@link ChartFeed} for a Polymarket outcome: paged price history bucketed into OHLC
44
+ * candles, plus a "live" candle. By default the latest bucket is re-fetched on an
45
+ * interval (Polymarket has no public candle socket). Pass `liveUrl` (a Livo indexer) to
46
+ * upgrade the live candle to INSTANT SSE updates. Price-history has no volume, so
47
+ * history candles carry `vol: 0`. Pass it to `connectFeed` with `bucketSeconds`.
48
+ */
49
+ export function polymarketFeed(opts) {
50
+ const secs = opts.bucketSeconds ?? 3600;
51
+ const host = opts.host ?? POLYMARKET_CLOB;
52
+ const tokenId = opts.tokenId;
53
+ const fidelity = Math.max(1, Math.round(secs / 60));
54
+ const ES = globalThis.EventSource;
55
+ return {
56
+ async loadHistory({ before, limit }) {
57
+ const endTs = Math.floor((before ?? opts.now ?? Date.now()) / 1000);
58
+ const startTs = endTs - secs * limit;
59
+ const points = await fetchPolymarketPriceHistory({ tokenId, host, startTs, endTs, fidelity });
60
+ return buildOHLC(points, secs);
61
+ },
62
+ subscribe(_params, onUpdate) {
63
+ // Poll the latest buckets from the public CLOB (the default + the SSE fallback).
64
+ const startPoll = () => {
65
+ const ms = opts.refetchMs ?? 30000;
66
+ if (ms <= 0)
67
+ return () => { };
68
+ let stopped = false;
69
+ let timer;
70
+ const tick = async () => {
71
+ if (stopped)
72
+ return;
73
+ try {
74
+ const endTs = Math.floor((opts.now ?? Date.now()) / 1000);
75
+ const points = await fetchPolymarketPriceHistory({ tokenId, host, startTs: endTs - secs * 3, endTs, fidelity });
76
+ const candles = buildOHLC(points, secs);
77
+ const last = candles[candles.length - 1];
78
+ if (last && !stopped)
79
+ onUpdate(last);
80
+ }
81
+ catch {
82
+ /* transient fetch error — try again next tick */
83
+ }
84
+ if (!stopped)
85
+ timer = setTimeout(tick, ms);
86
+ };
87
+ timer = setTimeout(tick, ms);
88
+ return () => {
89
+ stopped = true;
90
+ if (timer)
91
+ clearTimeout(timer);
92
+ };
93
+ };
94
+ // Realtime via the Livo indexer's SSE stream (instant), when configured + available.
95
+ // Falls back to polling if the stream can't connect (e.g. the indexer is down).
96
+ if (opts.liveUrl && ES)
97
+ return subscribeIndexerSse(opts.liveUrl, tokenId, secs, opts.now, onUpdate, ES, startPoll);
98
+ return startPoll();
99
+ },
100
+ };
101
+ }
102
+ /**
103
+ * Drive the live candle from Livo's Polymarket indexer SSE (`/stream`): `price` frames
104
+ * (mid) and `trade` frames (price + size → volume) for our token roll the in-progress
105
+ * bucket. If the stream can't connect (indexer down), it falls back to `startPoll` so the
106
+ * chart still gets a building candle. Returns an unsubscribe fn.
107
+ */
108
+ function subscribeIndexerSse(liveUrl, tokenId, secs, nowOverride, onUpdate, ES, startPoll) {
109
+ let cur = null;
110
+ let gotMessage = false;
111
+ let pollStop;
112
+ const es = new ES(`${liveUrl.replace(/\/$/, "")}/stream`);
113
+ // If the stream errors before ever delivering a frame, it's almost certainly down —
114
+ // close it and fall back to the poll. (A drop AFTER messages auto-reconnects via ES.)
115
+ es.onerror = () => {
116
+ if (!gotMessage && !pollStop) {
117
+ try {
118
+ es.close();
119
+ }
120
+ catch {
121
+ /* already closing */
122
+ }
123
+ pollStop = startPoll();
124
+ }
125
+ };
126
+ const tick = (price, vol = 0) => {
127
+ gotMessage = true;
128
+ if (!(price > 0) || !isFinite(price))
129
+ return;
130
+ const nowSec = Math.floor((nowOverride ?? Date.now()) / 1000);
131
+ const bt = Math.floor(nowSec / secs) * secs;
132
+ if (!cur || bt > cur.time)
133
+ cur = { time: bt, o: price, h: price, l: price, c: price, vol };
134
+ else {
135
+ cur.h = Math.max(cur.h, price);
136
+ cur.l = Math.min(cur.l, price);
137
+ cur.c = price;
138
+ cur.vol += vol;
139
+ }
140
+ onUpdate({ ...cur });
141
+ };
142
+ const onPrice = (ev) => {
143
+ try {
144
+ const d = JSON.parse(ev.data);
145
+ if (d.tokenId === tokenId)
146
+ tick(d.mid || d.last || 0);
147
+ }
148
+ catch {
149
+ /* ignore malformed frame */
150
+ }
151
+ };
152
+ const onTrade = (ev) => {
153
+ try {
154
+ const d = JSON.parse(ev.data);
155
+ if (d.tokenId === tokenId && d.price)
156
+ tick(d.price, d.size || 0);
157
+ }
158
+ catch {
159
+ /* ignore */
160
+ }
161
+ };
162
+ es.addEventListener("price", onPrice);
163
+ es.addEventListener("trade", onTrade);
164
+ return () => {
165
+ try {
166
+ es.close();
167
+ }
168
+ catch {
169
+ /* already closed */
170
+ }
171
+ pollStop?.();
172
+ };
173
+ }
@@ -1,4 +1,4 @@
1
- import type { Candle, ChartTheme, ChartType, Drawing, Indicator, Oscillator } from "./types";
1
+ import type { Candle, ChartTheme, ChartType, Drawing, Indicator, Oscillator, VolumeProfileConfig } from "./types";
2
2
  /**
3
3
  * Width of one candle slot (px). By default the slot is capped at `maxBarWidth` and the
4
4
  * series is right-anchored — sparse data stays tight in the corner. With `fitContent`,
@@ -8,13 +8,17 @@ import type { Candle, ChartTheme, ChartType, Drawing, Indicator, Oscillator } fr
8
8
  export declare function slotWidth(plotW: number, count: number, maxBarWidth: number, fitContent?: boolean): number;
9
9
  /** Convert a `#rrggbb` color to `rgba(...)` with the given alpha (passes other formats through). */
10
10
  export declare function withAlpha(color: string, alpha: number): string;
11
- /** The visible candle window (which slice is on screen and the per-candle slot width). */
11
+ /** The visible candle window (which slice is on screen and the per-candle slot width).
12
+ * A negative `view.offset` scrolls past the newest candle into the future: `end` exceeds
13
+ * the data length and `rightGap` counts the empty slots between the newest candle and the
14
+ * right edge (so the user can pull the current price left of the edge / toward center). */
12
15
  export declare function windowOf(candles: Candle[], view: Viewport, plotW: number, maxBarWidth: number, fitContent?: boolean): {
13
16
  start: number;
14
17
  end: number;
15
18
  vis: Candle[];
16
19
  n: number;
17
20
  count: number;
21
+ rightGap: number;
18
22
  cw: number;
19
23
  };
20
24
  /**
@@ -31,8 +35,9 @@ export declare function priceScale(vis: Candle[], yZoom: number, logScale: boole
31
35
  lo: number;
32
36
  hi: number;
33
37
  };
34
- /** Time ↔ pixel mapping for the visible window (candle index and arbitrary unix-second time). */
35
- export declare function timeScale(vis: Candle[], n: number, cw: number, plotW: number, interval: number): {
38
+ /** Time ↔ pixel mapping for the visible window (candle index and arbitrary unix-second time).
39
+ * `rightGap` (empty future slots, see windowOf) shifts the candles left of the right edge. */
40
+ export declare function timeScale(vis: Candle[], n: number, cw: number, plotW: number, interval: number, rightGap?: number): {
36
41
  xOf: (j: number) => number;
37
42
  xOfTime: (t: number) => number;
38
43
  timeOfX: (x: number) => number;
@@ -65,6 +70,10 @@ export interface RenderInput {
65
70
  width: number;
66
71
  height: number;
67
72
  candles: Candle[];
73
+ /** Candles used for the price series + axis (Heikin-Ashi in `heikin` mode). The Chart
74
+ * controller precomputes this on data/type change; if omitted, `draw` derives it. The
75
+ * RAW `candles` still drive volume, indicators, oscillators and the crosshair readout. */
76
+ priceCandles?: Candle[];
68
77
  view: Viewport;
69
78
  hover: {
70
79
  x: number;
@@ -88,6 +97,12 @@ export interface RenderInput {
88
97
  overlays?: ResolvedOverlay[];
89
98
  /** logarithmic price axis — equal vertical distance = equal % move. */
90
99
  logScale?: boolean;
100
+ /** centered text when there are no candles (default "no priced trades yet"; "" = nothing). */
101
+ emptyText?: string;
102
+ /** reference price for the `baseline` chart type (default: the first visible candle's close). */
103
+ baselinePrice?: number;
104
+ /** volume-by-price histogram drawn on the price pane (default off). */
105
+ volumeProfile?: VolumeProfileConfig;
91
106
  /** spread candles across the full plot width instead of right-anchoring at a capped slot. */
92
107
  fitContent?: boolean;
93
108
  /** y-axis price label formatter (default: range-aware {@link formatAxisValue}). */