@livo-build/charts 0.2.1 → 0.2.3

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.
@@ -0,0 +1,63 @@
1
+ import type { Candle, Point } from "./types";
2
+ import type { ChartFeed } from "./feed";
3
+ /** The public Livo signals engine. */
4
+ export declare const DEFAULT_SIGNALS_URL = "https://signals.livo.build";
5
+ /** The fields of a Signal Radar market this adapter needs. `spark` is recent closes (oldest→newest). */
6
+ export interface SignalsMarket {
7
+ pair: string;
8
+ base: string;
9
+ pool: string;
10
+ price: number;
11
+ spark: number[];
12
+ }
13
+ /** Fetch one Signal Radar market (price + spark) from the engine snapshot, or null if absent. */
14
+ export declare function fetchSignalsMarket(token: string, url?: string): Promise<SignalsMarket | null>;
15
+ /**
16
+ * Fetch a page of indexed swaps for a pool from the engine's GraphQL API (`allSwaps`,
17
+ * newest-first), mapped to priced {@link Point}s. `pool` must be the 0x-prefixed address.
18
+ * Returns one point per swap (unpriceable swaps keep `p: 0` and are dropped by `buildOHLC`),
19
+ * so the caller can advance its offset by `points.length`.
20
+ */
21
+ export declare function fetchSignalsSwaps(pool: string, opts?: {
22
+ url?: string;
23
+ first?: number;
24
+ offset?: number;
25
+ }): Promise<Point[]>;
26
+ /**
27
+ * Drop swaps whose price is a wild outlier — the engine occasionally mis-prices historical
28
+ * swaps (e.g. a thin pool's old trades priced at $2.5M for a $3 token). Uses the median of
29
+ * the newest swaps as a robust reference (recent prices are reliable) and rejects anything
30
+ * more than `factor`× off it. Input is newest-first; output preserves order.
31
+ */
32
+ export declare function rejectPriceOutliers(pts: Point[], factor?: number): Point[];
33
+ /** Turn a `spark` (recent closes, oldest→newest) into flat candles ending at `nowSec`. */
34
+ export declare function sparkCandles(spark: number[], bucketSeconds: number, nowSec: number, lastClose?: number): Candle[];
35
+ export interface SignalsFeedOptions {
36
+ /** Token to chart — pool address, base-token address, or pair symbol (e.g. "PEPE"). */
37
+ token: string;
38
+ /** Engine base URL (default https://signals.livo.build). */
39
+ url?: string;
40
+ /** Candle bucket size in seconds (default 300 = 5m). */
41
+ bucketSeconds?: number;
42
+ /** Swaps fetched per history page from the index (default 1500); older pages load lazily. */
43
+ swapPageSize?: number;
44
+ /** Candles to load before the first paint, so the chart opens filled rather than a sliver
45
+ * on the right (default 160). Active pools pack many swaps per bucket, so this may take a
46
+ * few index pages. */
47
+ initialCandles?: number;
48
+ /** Force `"spark"` (snapshot closes only) instead of the default `"index"` (GraphQL history). */
49
+ history?: "index" | "spark";
50
+ /** Live poll cadence in ms (default 15s; 0 disables the live poll). */
51
+ refetchMs?: number;
52
+ /** Override "now" (ms) — for tests/determinism. */
53
+ now?: number;
54
+ }
55
+ /**
56
+ * A {@link ChartFeed} for a Signal Radar token. History comes from the engine's indexed
57
+ * swaps (`/graphql allSwaps` for the pool → bucketed into OHLC at `bucketSeconds`), paged
58
+ * lazily on back-scroll, so it shows real candles days deep. The live candle is polled from
59
+ * `/data` (current price). If the index is unreachable (or `history: "spark"`), it falls back
60
+ * to seeding from the snapshot's `spark`. Pass it to `connectFeed` with `bucketSeconds` as
61
+ * the interval.
62
+ */
63
+ export declare function signalsFeed(opts: SignalsFeedOptions): ChartFeed;
@@ -0,0 +1,234 @@
1
+ // Signal Radar adapter — chart a token tracked by the Livo on-chain signals engine.
2
+ // The engine ("Signal Radar") indexes every Ethereum mainnet DEX swap into Postgres and
3
+ // exposes it over a PostGraphile GraphQL API (`/graphql`), plus a live `/data` snapshot
4
+ // (no key) with the current USD `price` and a short `spark` of recent closes.
5
+ //
6
+ // This adapter pulls REAL history from the index — `allSwaps` for the pool, bucketed into
7
+ // OHLC candles at any interval — so it shows true 1m / 5m candles days back (the index has
8
+ // no TTL), and lazy-loads older pages as you scroll. It polls `/data` for the live, building
9
+ // candle. If `/graphql` is unreachable (e.g. CORS), it falls back to seeding from `spark`.
10
+ //
11
+ // import { Chart, connectFeed, signalsFeed } from "@livo-build/charts";
12
+ // const chart = new Chart(el, { height: 420 });
13
+ // connectFeed(chart, signalsFeed({ token: "PEPE" }), { interval: 300 });
14
+ import { buildOHLC } from "./ohlc";
15
+ /** The public Livo signals engine. */
16
+ export const DEFAULT_SIGNALS_URL = "https://signals.livo.build";
17
+ /** fetch with an abort timeout so a stalled engine request can't wedge lazy-loading forever. */
18
+ async function fetchT(input, init = {}, timeoutMs = 12000) {
19
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
20
+ const timer = ac ? setTimeout(() => ac.abort(), timeoutMs) : undefined;
21
+ try {
22
+ return await fetch(input, ac ? { ...init, signal: ac.signal } : init);
23
+ }
24
+ finally {
25
+ if (timer)
26
+ clearTimeout(timer);
27
+ }
28
+ }
29
+ const bare = (s) => s.replace(/^0x/i, "").toLowerCase();
30
+ /** Find the snapshot market for a token by pool address, base-token address, or pair symbol. */
31
+ function findMarket(markets, token) {
32
+ const q = bare(token);
33
+ const byAddr = markets.find((m) => bare(m.pool) === q || bare(m.base) === q);
34
+ if (byAddr)
35
+ return byAddr;
36
+ const sym = token.toLowerCase();
37
+ // symbol match → the deepest (first listed; the snapshot is hottest-first) market.
38
+ return markets.find((m) => m.pair.toLowerCase().split("/")[0] === sym);
39
+ }
40
+ /** Fetch one Signal Radar market (price + spark) from the engine snapshot, or null if absent. */
41
+ export async function fetchSignalsMarket(token, url = DEFAULT_SIGNALS_URL) {
42
+ const res = await fetchT(`${url}/data`);
43
+ if (!res.ok)
44
+ throw new Error("signals /data " + res.status);
45
+ const snap = (await res.json());
46
+ return findMarket(snap.markets ?? [], token) ?? null;
47
+ }
48
+ /**
49
+ * Fetch a page of indexed swaps for a pool from the engine's GraphQL API (`allSwaps`,
50
+ * newest-first), mapped to priced {@link Point}s. `pool` must be the 0x-prefixed address.
51
+ * Returns one point per swap (unpriceable swaps keep `p: 0` and are dropped by `buildOHLC`),
52
+ * so the caller can advance its offset by `points.length`.
53
+ */
54
+ export async function fetchSignalsSwaps(pool, opts = {}) {
55
+ const url = opts.url ?? DEFAULT_SIGNALS_URL;
56
+ const first = opts.first ?? 1500;
57
+ const offset = opts.offset ?? 0;
58
+ const query = `{ allSwaps(condition:{pool:"${pool}"}, orderBy: BLOCK_TIME_DESC, first:${first}, offset:${offset}){ nodes{ blockTime priceUsd amountUsd } } }`;
59
+ const res = await fetchT(`${url}/graphql`, {
60
+ method: "POST",
61
+ headers: { "content-type": "application/json" },
62
+ body: JSON.stringify({ query }),
63
+ });
64
+ if (!res.ok)
65
+ throw new Error("signals /graphql " + res.status);
66
+ const j = (await res.json());
67
+ const nodes = j?.data?.allSwaps?.nodes;
68
+ if (!nodes)
69
+ throw new Error("signals /graphql: no allSwaps");
70
+ return nodes.map((s) => ({ t: +s.blockTime, p: +(s.priceUsd ?? 0), v: +(s.amountUsd ?? 0) || 0 }));
71
+ }
72
+ /**
73
+ * Drop swaps whose price is a wild outlier — the engine occasionally mis-prices historical
74
+ * swaps (e.g. a thin pool's old trades priced at $2.5M for a $3 token). Uses the median of
75
+ * the newest swaps as a robust reference (recent prices are reliable) and rejects anything
76
+ * more than `factor`× off it. Input is newest-first; output preserves order.
77
+ */
78
+ export function rejectPriceOutliers(pts, factor = 100) {
79
+ const priced = pts.filter((p) => p.p > 0 && isFinite(p.p));
80
+ if (priced.length < 8)
81
+ return priced;
82
+ const recent = priced
83
+ .slice(0, 200)
84
+ .map((p) => p.p)
85
+ .sort((a, b) => a - b);
86
+ const ref = recent[Math.floor(recent.length / 2)] || 0;
87
+ if (!(ref > 0))
88
+ return priced;
89
+ return priced.filter((p) => p.p >= ref / factor && p.p <= ref * factor);
90
+ }
91
+ /** Turn a `spark` (recent closes, oldest→newest) into flat candles ending at `nowSec`. */
92
+ export function sparkCandles(spark, bucketSeconds, nowSec, lastClose) {
93
+ const n = spark.length;
94
+ const endBucket = Math.floor(nowSec / bucketSeconds) * bucketSeconds;
95
+ return spark.map((p, i) => {
96
+ const c = i === n - 1 && lastClose != null ? lastClose : p;
97
+ const time = endBucket - (n - 1 - i) * bucketSeconds;
98
+ return { time, o: p, h: Math.max(p, c), l: Math.min(p, c), c, vol: 0 };
99
+ });
100
+ }
101
+ /**
102
+ * A {@link ChartFeed} for a Signal Radar token. History comes from the engine's indexed
103
+ * swaps (`/graphql allSwaps` for the pool → bucketed into OHLC at `bucketSeconds`), paged
104
+ * lazily on back-scroll, so it shows real candles days deep. The live candle is polled from
105
+ * `/data` (current price). If the index is unreachable (or `history: "spark"`), it falls back
106
+ * to seeding from the snapshot's `spark`. Pass it to `connectFeed` with `bucketSeconds` as
107
+ * the interval.
108
+ */
109
+ export function signalsFeed(opts) {
110
+ const url = opts.url ?? DEFAULT_SIGNALS_URL;
111
+ const secs = opts.bucketSeconds ?? 300;
112
+ const token = opts.token;
113
+ const swapPage = opts.swapPageSize ?? 1500;
114
+ const initialCandles = opts.initialCandles ?? 160;
115
+ const sparkOnly = opts.history === "spark";
116
+ const MAX_SWAPS = 60000; // memory/CPU bound on deep back-scroll
117
+ // Resolve the market once (gives the pool to query + the spark fallback), cached.
118
+ let marketP = null;
119
+ const market = () => (marketP ?? (marketP = fetchSignalsMarket(token, url).catch(() => null)));
120
+ // Accumulate ALL fetched swaps and re-bucket the full set each page, so a candle bucket
121
+ // that straddles a swap-page boundary is always COMPLETE (no partial O/H/L/volume). The
122
+ // feed returns the full ascending candle set; connectFeed merges it (replacing the partial
123
+ // boundary it already showed). `offset` pages older swaps; bounded by MAX_SWAPS.
124
+ // When `token` is a pool address, query the index by it DIRECTLY — the index keys on the
125
+ // pool (permanent) so history works even after the market rotates out of the live `/data`
126
+ // snapshot. /data is then only needed for the spark fallback + the live price.
127
+ const directPool = /^(0x)?[0-9a-fA-F]{40}$/.test(token) ? `0x${bare(token)}` : null;
128
+ let allSwaps = [];
129
+ let offset = 0;
130
+ let sparkMode = sparkOnly; // flipped on if the index is unreachable
131
+ let histLatest = null; // newest indexed candle (seeds the live bucket)
132
+ const spark = async (m) => {
133
+ if (!m)
134
+ return [];
135
+ const nowSec = (opts.now ?? Date.now()) / 1000;
136
+ const cs = sparkCandles(m.spark ?? [], secs, nowSec, m.price);
137
+ histLatest = cs[cs.length - 1] ?? null;
138
+ return cs;
139
+ };
140
+ return {
141
+ async loadHistory({ before }) {
142
+ const m = await market(); // for the spark fallback + the live price (best-effort)
143
+ // Prefer the pool straight from the token (rotation-proof); else resolve it via /data.
144
+ const pool = directPool ?? (m && m.pool ? `0x${bare(m.pool)}` : null);
145
+ if (!pool)
146
+ return [];
147
+ // Spark mode (forced via `history:"spark"`, or fell back): only the initial page has
148
+ // data (the snapshot's recent closes); there are no deeper pages.
149
+ if (sparkMode)
150
+ return before == null ? spark(m) : [];
151
+ if (allSwaps.length >= MAX_SWAPS)
152
+ return []; // depth cap reached
153
+ try {
154
+ // On the FIRST load, keep pulling pages until we have a full screen of candles, so the
155
+ // chart paints filled rather than a sliver on the right (active pools pack many swaps
156
+ // into each bucket, so one page can be only an hour or two). Lazy scroll-back fetches
157
+ // one page at a time. `rejectPriceOutliers` drops engine mis-pricings (e.g. $2.5M).
158
+ const want = before == null ? initialCandles : 0;
159
+ let got = 0;
160
+ do {
161
+ const pts = await fetchSignalsSwaps(pool, { url, first: swapPage, offset });
162
+ offset += pts.length;
163
+ got += pts.length;
164
+ if (pts.length === 0)
165
+ break;
166
+ allSwaps = allSwaps.concat(pts);
167
+ } while (allSwaps.length < MAX_SWAPS && buildOHLC(rejectPriceOutliers(allSwaps), secs).length < want);
168
+ if (got === 0) {
169
+ if (allSwaps.length === 0) {
170
+ sparkMode = true; // index empty for this pool — seed from spark
171
+ return before == null ? spark(m) : [];
172
+ }
173
+ return []; // no older swaps — history exhausted
174
+ }
175
+ const candles = buildOHLC(rejectPriceOutliers(allSwaps), secs); // clean, complete buckets
176
+ histLatest = candles[candles.length - 1] ?? null; // newest (may be the live bucket) — seeds the live poll
177
+ // EXCLUDE the in-progress (current wall-clock) bucket: the live poll owns it. Otherwise a
178
+ // history re-fetch (lazy load) would overwrite the live candle with the stale indexed close,
179
+ // and the next poll would restore the live price — making the latest candle oscillate.
180
+ const curBucket = Math.floor((opts.now ?? Date.now()) / 1000 / secs) * secs;
181
+ return candles.filter((c) => c.time < curBucket);
182
+ }
183
+ catch {
184
+ // index unreachable (e.g. /graphql CORS) — fall back to the snapshot spark, once.
185
+ sparkMode = true;
186
+ return before == null ? spark(m) : [];
187
+ }
188
+ },
189
+ subscribe(_params, onUpdate) {
190
+ const ms = opts.refetchMs ?? 15000;
191
+ if (ms <= 0)
192
+ return () => { };
193
+ let stopped = false;
194
+ let timer;
195
+ // Track the in-progress bucket so polled prices accumulate high/low instead of
196
+ // overwriting the whole candle each tick.
197
+ let cur = null;
198
+ const tick = async () => {
199
+ if (stopped)
200
+ return;
201
+ try {
202
+ const m = await fetchSignalsMarket(token, url);
203
+ if (m && !stopped) {
204
+ const nowSec = (opts.now ?? Date.now()) / 1000;
205
+ const bt = Math.floor(nowSec / secs) * secs;
206
+ if (!cur || cur.time !== bt) {
207
+ // Seed the in-progress bucket from the latest INDEXED candle when it's the same
208
+ // bucket, so the live price extends real OHLC/volume instead of flattening it.
209
+ const base = histLatest && histLatest.time === bt ? histLatest : null;
210
+ cur = base
211
+ ? { ...base, h: Math.max(base.h, m.price), l: Math.min(base.l, m.price), c: m.price }
212
+ : { time: bt, o: m.price, h: m.price, l: m.price, c: m.price, vol: 0 };
213
+ }
214
+ else {
215
+ cur = { ...cur, h: Math.max(cur.h, m.price), l: Math.min(cur.l, m.price), c: m.price };
216
+ }
217
+ onUpdate(cur);
218
+ }
219
+ }
220
+ catch {
221
+ /* transient fetch error — try again next tick */
222
+ }
223
+ if (!stopped)
224
+ timer = setTimeout(tick, ms);
225
+ };
226
+ void tick(); // fire immediately so the live (current-bucket) candle appears at once
227
+ return () => {
228
+ stopped = true;
229
+ if (timer)
230
+ clearTimeout(timer);
231
+ };
232
+ },
233
+ };
234
+ }
@@ -1,3 +1,7 @@
1
1
  import type { ChartTheme } from "./types";
2
- /** Default dark theme (TradingView-ish palette). */
2
+ /** Default dark theme — a refined, modern crypto-terminal palette. */
3
3
  export declare const DEFAULT_THEME: ChartTheme;
4
+ /** Light theme preset (same hues on a clean white plot). Pass to `theme` / `setTheme`. */
5
+ export declare const LIGHT_THEME: ChartTheme;
6
+ /** Built-in theme presets, addressable by name. */
7
+ export declare const THEME_PRESETS: Record<"dark" | "light", ChartTheme>;
@@ -1,14 +1,35 @@
1
- /** Default dark theme (TradingView-ish palette). */
1
+ /** Default dark theme — a refined, modern crypto-terminal palette. */
2
2
  export const DEFAULT_THEME = {
3
- up: "#26a69a",
4
- down: "#ef5350",
5
- line: "#2962ff",
6
- grid: "#1c2030",
7
- axis: "#787b86",
8
- crosshair: "#9598a1",
9
- volUp: "rgba(38,166,154,.5)",
10
- volDown: "rgba(239,83,80,.5)",
11
- tagText: "#0a0a0a",
12
- axisTagBg: "#2a2e39",
13
- axisTagText: "#d1d4dc",
3
+ up: "#2ebd85",
4
+ down: "#f6465d",
5
+ line: "#5b8def",
6
+ grid: "#1a2030",
7
+ axis: "#8b919e",
8
+ crosshair: "#b3b9c4",
9
+ volUp: "rgba(46,189,133,.28)",
10
+ volDown: "rgba(246,70,93,.28)",
11
+ tagText: "#06080c",
12
+ axisTagBg: "#2b3242",
13
+ axisTagText: "#eaedf2",
14
+ background: "#0b0e14",
15
+ };
16
+ /** Light theme preset (same hues on a clean white plot). Pass to `theme` / `setTheme`. */
17
+ export const LIGHT_THEME = {
18
+ up: "#089981",
19
+ down: "#f23645",
20
+ line: "#3b6fe0",
21
+ grid: "#eceff4",
22
+ axis: "#6b7280",
23
+ crosshair: "#4b5563",
24
+ volUp: "rgba(8,153,129,.22)",
25
+ volDown: "rgba(242,54,69,.22)",
26
+ tagText: "#ffffff",
27
+ axisTagBg: "#1f2733",
28
+ axisTagText: "#ffffff",
29
+ background: "#ffffff",
30
+ };
31
+ /** Built-in theme presets, addressable by name. */
32
+ export const THEME_PRESETS = {
33
+ dark: DEFAULT_THEME,
34
+ light: LIGHT_THEME,
14
35
  };
@@ -15,10 +15,30 @@ export interface Point {
15
15
  v?: number;
16
16
  }
17
17
  export type Denom = "USD" | "ETH";
18
- export type ChartType = "candle" | "line";
19
- export type IndicatorType = "sma" | "ema" | "wma" | "vwap";
18
+ /** Price-series style: candlesticks, a line, a two-tone baseline area, or Heikin-Ashi candles. */
19
+ export type ChartType = "candle" | "line" | "baseline" | "heikin";
20
+ export type IndicatorType = "sma" | "ema" | "wma" | "vwap" | "bollinger";
20
21
  export type PriceSource = "open" | "high" | "low" | "close";
21
- /** An overlay indicator drawn on the price pane (a moving average for now). */
22
+ /** Oscillators render in their own stacked sub-pane below the volume panel. */
23
+ export type OscillatorType = "rsi" | "macd" | "stoch" | "atr";
24
+ /** An oscillator drawn in a dedicated sub-pane (RSI/Stochastic 0–100 band, MACD, or ATR). */
25
+ export interface Oscillator {
26
+ type: OscillatorType;
27
+ /** Lookback — RSI/ATR period, or the Stochastic %K period (defaults 14). Ignored by MACD. */
28
+ period?: number;
29
+ /** MACD fast / slow / signal EMA lengths (defaults 12 / 26 / 9). Ignored by the others. */
30
+ fast?: number;
31
+ slow?: number;
32
+ signal?: number;
33
+ /** Stochastic %D period (default 3) and %K smoothing (default 1 = fast). Ignored by others. */
34
+ dPeriod?: number;
35
+ smooth?: number;
36
+ /** Pane height in px (default 84). */
37
+ height?: number;
38
+ /** Primary line color; defaults to a built-in palette slot. */
39
+ color?: string;
40
+ }
41
+ /** An overlay indicator drawn on the price pane (a moving average, VWAP, or Bollinger band). */
22
42
  export interface Indicator {
23
43
  type: IndicatorType;
24
44
  /** Lookback in candles. */
@@ -27,7 +47,29 @@ export interface Indicator {
27
47
  color?: string;
28
48
  /** Which price to average. Default "close". */
29
49
  source?: PriceSource;
50
+ /** Bollinger band width in standard deviations (default 2). Ignored by other types. */
51
+ mult?: number;
52
+ }
53
+ /** A point anchored in data space (so drawings stay attached across pan/zoom). */
54
+ export interface DrawingPoint {
55
+ /** unix seconds. */
56
+ time: number;
57
+ price: number;
30
58
  }
59
+ export type DrawingType = "trendline" | "hline" | "fib" | "rect";
60
+ /**
61
+ * A user-drawn annotation on the price pane. `hline` uses `a.price`; `trendline`, `fib`
62
+ * (retracement levels between the two prices) and `rect` (box) use both `a` and `b`.
63
+ */
64
+ export interface Drawing {
65
+ id: string;
66
+ type: DrawingType;
67
+ a: DrawingPoint;
68
+ b?: DrawingPoint;
69
+ color?: string;
70
+ }
71
+ /** Drawing interaction mode. `none` = pan/zoom + select/move; the others arm a one-shot draw. */
72
+ export type DrawMode = "none" | "trendline" | "hline" | "fib" | "rect";
31
73
  /** Transform applied to raw prices before aggregation. */
32
74
  export interface PriceTransform {
33
75
  /** "USD" leaves prices as-is; "ETH" divides by `ethUsd`. */
@@ -56,6 +98,15 @@ export interface ChartTheme {
56
98
  axisTagText: string;
57
99
  background?: string;
58
100
  }
101
+ /** Volume-by-price overlay config. `true` uses defaults; an object tunes it. */
102
+ export type VolumeProfileConfig = boolean | {
103
+ /** number of price bands (default 24). */
104
+ buckets?: number;
105
+ /** fraction of the plot width the widest bar occupies (default 0.16). */
106
+ width?: number;
107
+ /** bar fill color (default a translucent `theme.axis`). */
108
+ color?: string;
109
+ };
59
110
  export interface ChartOptions {
60
111
  /** canvas height in CSS px (default 420). */
61
112
  height?: number;
@@ -66,6 +117,11 @@ export interface ChartOptions {
66
117
  minBars?: number;
67
118
  /** cap on a candle's slot width in px — keeps sparse series tight, right-anchored (default 18). */
68
119
  maxBarWidth?: number;
120
+ /** cap on a candle's BODY (and volume bar) width in px (default 40). The body is 70% of
121
+ * the slot up to this cap; raise it for chunkier candles on sparse / `fitContent` series. */
122
+ maxBodyWidth?: number;
123
+ /** show the volume panel (default true). */
124
+ showVolume?: boolean;
69
125
  /** fraction of the plot used by the volume panel (default 0.18). */
70
126
  volumeRatio?: number;
71
127
  rightPad?: number;
@@ -75,6 +131,49 @@ export interface ChartOptions {
75
131
  onCrosshair?: (candle: Candle | null) => void;
76
132
  /** moving-average overlays drawn on the price pane. */
77
133
  indicators?: Indicator[];
134
+ /** oscillators (RSI / MACD) drawn in stacked sub-panes below the volume panel. */
135
+ oscillators?: Oscillator[];
136
+ /** user drawings (trendlines / horizontal price lines) to render on the price pane. */
137
+ drawings?: Drawing[];
138
+ /** fired whenever the user adds, moves, or deletes a drawing. */
139
+ onDrawingsChange?: (drawings: Drawing[]) => void;
140
+ /** fired when the active draw mode changes (e.g. auto-reset to "none" after one draw). */
141
+ onDrawModeChange?: (mode: DrawMode) => void;
142
+ /** logarithmic price axis — equal vertical distance = equal % move (default false). */
143
+ logScale?: boolean;
144
+ /** centered text shown when there are no candles (default "no priced trades yet"). Feeds
145
+ * set this to "Loading…" while the first page is in flight. Pass "" to draw nothing. */
146
+ emptyText?: string;
147
+ /** reference price for the `baseline` chart type (default: the first visible candle's close). */
148
+ baselinePrice?: number;
149
+ /** volume-by-price histogram drawn on the price pane (default off). */
150
+ volumeProfile?: VolumeProfileConfig;
151
+ /** spread candles across the full plot width instead of right-anchoring a capped slot
152
+ * (default false in the core `Chart`; the React wrappers default it on). */
153
+ fitContent?: boolean;
154
+ /** y-axis price label formatter. Default adapts precision to the tick step. */
155
+ priceFormat?: (value: number) => string;
156
+ /** x-axis time label formatter. Default {@link formatTime}. */
157
+ timeFormat?: (time: number, interval: number) => string;
158
+ /** number of horizontal price gridlines / labels (default 5). */
159
+ priceTicks?: number;
160
+ /** approximate number of time-axis labels (default 7). */
161
+ timeTicks?: number;
162
+ /** canvas font for axis labels (default "10px ui-monospace, monospace"); color is `theme.axis`. */
163
+ axisFont?: string;
78
164
  /** fired when the user pans/zooms near the start of loaded data (lazy history). */
79
165
  onNeedHistory?: () => void;
80
166
  }
167
+ /** Axis label/format/layout overrides — the subset of {@link ChartOptions} that styles the ticks. */
168
+ export interface AxisOptions {
169
+ fitContent?: boolean;
170
+ priceFormat?: (value: number) => string;
171
+ timeFormat?: (time: number, interval: number) => string;
172
+ priceTicks?: number;
173
+ timeTicks?: number;
174
+ axisFont?: string;
175
+ /** max candle slot width in px (spacing). */
176
+ maxBarWidth?: number;
177
+ /** max candle body / volume-bar width in px. */
178
+ maxBodyWidth?: number;
179
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,17 @@
1
- export { Chart } from "./core/chart";
2
- export { buildOHLC, transformPrice } from "./core/ohlc";
3
- export { draw } from "./core/renderer";
4
- export type { RenderInput, Viewport, Pads, ResolvedOverlay } from "./core/renderer";
1
+ export { Chart, needsHistory, maxPanOffset } from "./core/chart";
2
+ export { buildOHLC, transformPrice, heikinAshi } from "./core/ohlc";
3
+ export { draw, slotWidth, windowOf, priceScale, timeScale, computeProjection, withAlpha, resolveOverlays } from "./core/renderer";
4
+ export type { RenderInput, Viewport, Pads, ResolvedOverlay, Projection } from "./core/renderer";
5
5
  export { connectFeed } from "./core/feed";
6
6
  export type { ChartFeed, ConnectFeedOptions, FeedConnection, LoadHistoryParams } from "./core/feed";
7
- export { DEFAULT_THEME } from "./core/theme";
8
- export { formatValue, formatVolume, formatTime } from "./core/format";
9
- export { sma, ema, wma, vwap, bollingerBands, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
7
+ export { DEFAULT_THEME, LIGHT_THEME, THEME_PRESETS } from "./core/theme";
8
+ export { formatValue, formatAxisValue, formatVolume, formatTime } from "./core/format";
9
+ export { sma, ema, wma, vwap, bollingerBands, rsi, macd, stochastic, atr, volumeProfile, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
10
+ export type { VolumeBucket, VolumeProfileResult } from "./core/indicators";
10
11
  export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
11
12
  export type { HlRawCandle, FetchHlCandlesOptions, HyperliquidFeedOptions } from "./core/live";
12
- export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, } from "./core/types";
13
+ export { fetchPolymarketPriceHistory, polymarketFeed, POLYMARKET_CLOB } from "./core/polymarket";
14
+ export type { PolyHistoryPoint, FetchPolyHistoryOptions, PolymarketFeedOptions } from "./core/polymarket";
15
+ export { fetchSignalsMarket, fetchSignalsSwaps, signalsFeed, sparkCandles, rejectPriceOutliers, DEFAULT_SIGNALS_URL } from "./core/signals";
16
+ export type { SignalsMarket, SignalsFeedOptions } from "./core/signals";
17
+ export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, Oscillator, OscillatorType, Drawing, DrawingType, DrawingPoint, DrawMode, AxisOptions, VolumeProfileConfig, } from "./core/types";
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  // livo-charts — framework-agnostic core.
2
2
  // React bindings live in the `@livo-build/charts/react` subpath so non-React
3
3
  // consumers never pull React into their bundle.
4
- export { Chart } from "./core/chart";
5
- export { buildOHLC, transformPrice } from "./core/ohlc";
6
- export { draw } from "./core/renderer";
4
+ export { Chart, needsHistory, maxPanOffset } from "./core/chart";
5
+ export { buildOHLC, transformPrice, heikinAshi } from "./core/ohlc";
6
+ export { draw, slotWidth, windowOf, priceScale, timeScale, computeProjection, withAlpha, resolveOverlays } from "./core/renderer";
7
7
  export { connectFeed } from "./core/feed";
8
- export { DEFAULT_THEME } from "./core/theme";
9
- export { formatValue, formatVolume, formatTime } from "./core/format";
10
- export { sma, ema, wma, vwap, bollingerBands, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
8
+ export { DEFAULT_THEME, LIGHT_THEME, THEME_PRESETS } from "./core/theme";
9
+ export { formatValue, formatAxisValue, formatVolume, formatTime } from "./core/format";
10
+ export { sma, ema, wma, vwap, bollingerBands, rsi, macd, stochastic, atr, volumeProfile, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
11
11
  export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
12
+ export { fetchPolymarketPriceHistory, polymarketFeed, POLYMARKET_CLOB } from "./core/polymarket";
13
+ export { fetchSignalsMarket, fetchSignalsSwaps, signalsFeed, sparkCandles, rejectPriceOutliers, DEFAULT_SIGNALS_URL } from "./core/signals";
@@ -1,4 +1,4 @@
1
- import type { ChartTheme, ChartType, Indicator } from "../core/types";
1
+ import type { ChartTheme, ChartType, Drawing, Indicator, Oscillator, VolumeProfileConfig } from "../core/types";
2
2
  export interface HyperliquidChartProps {
3
3
  /** Hyperliquid coin symbol, e.g. "BTC", "ETH", or a builder-dex name like "xyz:TSLA". */
4
4
  coin: string;
@@ -9,11 +9,39 @@ export interface HyperliquidChartProps {
9
9
  chartType?: ChartType;
10
10
  /** Moving-average overlays, e.g. [{ type: "ema", period: 21 }]. */
11
11
  indicators?: Indicator[];
12
+ /** Oscillator sub-panes (RSI / MACD), e.g. [{ type: "macd" }]. */
13
+ oscillators?: Oscillator[];
14
+ /** Initial drawings (trendlines / horizontal lines). */
15
+ drawings?: Drawing[];
16
+ /** Fired whenever the user adds, moves, or deletes a drawing. */
17
+ onDrawingsChange?: (drawings: Drawing[]) => void;
18
+ /** Hide the trendline / h-line / clear drawing-tool buttons (shown by default). */
19
+ hideDrawingTools?: boolean;
20
+ /** Show the volume panel (default true). */
21
+ showVolume?: boolean;
22
+ /** Volume-by-price histogram (default off); the toolbar adds a VP toggle when set. */
23
+ volumeProfile?: VolumeProfileConfig;
12
24
  /** Candles per history page; older pages load as you scroll left (default 500). */
13
25
  pageSize?: number;
14
26
  theme?: Partial<ChartTheme>;
15
27
  /** Show the interval / candle-line toolbar. Default true. */
16
28
  toolbar?: boolean;
29
+ /** Spread candles across the full width instead of right-anchoring (default true). */
30
+ fitContent?: boolean;
31
+ /** Max candle slot width in px (spacing; default 18). */
32
+ maxBarWidth?: number;
33
+ /** Max candle body / volume-bar width in px (default 40). */
34
+ maxBodyWidth?: number;
35
+ /** y-axis price label formatter (default adapts precision to the tick step). */
36
+ priceFormat?: (value: number) => string;
37
+ /** x-axis time label formatter. */
38
+ timeFormat?: (time: number, interval: number) => string;
39
+ /** number of horizontal price gridlines / labels (default 5). */
40
+ priceTicks?: number;
41
+ /** approximate number of time-axis labels (default 7). */
42
+ timeTicks?: number;
43
+ /** canvas font for axis labels; color is `theme.axis`. */
44
+ axisFont?: string;
17
45
  }
18
46
  /**
19
47
  * A turnkey, REALTIME Hyperliquid trading chart. Streams live candles over WebSocket,
@@ -25,4 +53,4 @@ export interface HyperliquidChartProps {
25
53
  * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
26
54
  * ```
27
55
  */
28
- export declare function HyperliquidChart({ coin, interval, testnet, height, chartType, indicators, pageSize, theme, toolbar, }: HyperliquidChartProps): import("react").JSX.Element;
56
+ export declare function HyperliquidChart({ coin, interval, testnet, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, pageSize, theme, toolbar, fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, }: HyperliquidChartProps): import("react").JSX.Element;