@livo-build/charts 0.2.1
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.
- package/README.md +189 -0
- package/dist/core/chart.d.ts +81 -0
- package/dist/core/chart.js +299 -0
- package/dist/core/feed.d.ts +43 -0
- package/dist/core/feed.js +87 -0
- package/dist/core/format.d.ts +6 -0
- package/dist/core/format.js +40 -0
- package/dist/core/indicators.d.ts +36 -0
- package/dist/core/indicators.js +112 -0
- package/dist/core/live.d.ts +60 -0
- package/dist/core/live.js +107 -0
- package/dist/core/ohlc.d.ts +9 -0
- package/dist/core/ohlc.js +36 -0
- package/dist/core/renderer.d.ts +49 -0
- package/dist/core/renderer.js +203 -0
- package/dist/core/theme.d.ts +3 -0
- package/dist/core/theme.js +14 -0
- package/dist/core/types.d.ts +80 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/react/HyperliquidChart.d.ts +28 -0
- package/dist/react/HyperliquidChart.js +79 -0
- package/dist/react/PriceChart.d.ts +31 -0
- package/dist/react/PriceChart.js +87 -0
- package/dist/react.d.ts +4 -0
- package/dist/react.js +3 -0
- package/package.json +67 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Feed connector — drive a Chart from a data source that can serve paged OHLCV
|
|
2
|
+
// history AND live updates. `connectFeed` does the plumbing: an initial page, lazy
|
|
3
|
+
// loading of older candles as the user scrolls left, and merging live candles
|
|
4
|
+
// (updating the in-progress bucket or appending a new one). Dependency-free.
|
|
5
|
+
// Merge a candle into an ascending buffer: replace the same-time bucket, or append
|
|
6
|
+
// a strictly-newer one. Returns a NEW array iff something changed, else the same ref.
|
|
7
|
+
function mergeLive(buf, c) {
|
|
8
|
+
const last = buf[buf.length - 1];
|
|
9
|
+
if (last && c.time === last.time) {
|
|
10
|
+
const next = buf.slice();
|
|
11
|
+
next[next.length - 1] = c;
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
if (!last || c.time > last.time)
|
|
15
|
+
return [...buf, c];
|
|
16
|
+
return buf; // stale (older than last) — ignore
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wire a {@link ChartFeed} to a {@link Chart}: load the latest page, lazily prepend
|
|
20
|
+
* older candles when the user scrolls near the start, and merge live updates. The
|
|
21
|
+
* chart's right-anchored view means prepending history keeps the visible window
|
|
22
|
+
* stable automatically.
|
|
23
|
+
*/
|
|
24
|
+
export function connectFeed(chart, feed, opts) {
|
|
25
|
+
const limit = opts.pageSize ?? 500;
|
|
26
|
+
let buf = [];
|
|
27
|
+
let loadingOlder = false;
|
|
28
|
+
let exhausted = false;
|
|
29
|
+
let unsub;
|
|
30
|
+
let live = true;
|
|
31
|
+
const push = (next) => {
|
|
32
|
+
buf = next;
|
|
33
|
+
if (!live)
|
|
34
|
+
return;
|
|
35
|
+
chart.setCandles(buf);
|
|
36
|
+
opts.onCandles?.(buf);
|
|
37
|
+
};
|
|
38
|
+
const loadOlder = async () => {
|
|
39
|
+
if (loadingOlder || exhausted)
|
|
40
|
+
return;
|
|
41
|
+
loadingOlder = true;
|
|
42
|
+
try {
|
|
43
|
+
const before = buf.length ? buf[0].time * 1000 : undefined;
|
|
44
|
+
const older = await feed.loadHistory({ interval: opts.interval, before, limit });
|
|
45
|
+
const seen = new Set(buf.map((c) => c.time));
|
|
46
|
+
const fresh = older.filter((c) => !seen.has(c.time)).sort((a, b) => a.time - b.time);
|
|
47
|
+
if (fresh.length === 0)
|
|
48
|
+
exhausted = true;
|
|
49
|
+
else
|
|
50
|
+
push([...fresh, ...buf]);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
opts.onError?.(e);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
loadingOlder = false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
chart.setInterval(opts.interval);
|
|
60
|
+
chart.setNeedHistory(() => void loadOlder());
|
|
61
|
+
// initial page + live subscription
|
|
62
|
+
void (async () => {
|
|
63
|
+
try {
|
|
64
|
+
const first = await feed.loadHistory({ interval: opts.interval, limit });
|
|
65
|
+
push(first.slice().sort((a, b) => a.time - b.time));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
opts.onError?.(e);
|
|
69
|
+
}
|
|
70
|
+
if (feed.subscribe) {
|
|
71
|
+
unsub = feed.subscribe({ interval: opts.interval }, (c) => {
|
|
72
|
+
const next = mergeLive(buf, c);
|
|
73
|
+
if (next !== buf)
|
|
74
|
+
push(next);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
return {
|
|
79
|
+
disconnect() {
|
|
80
|
+
live = false;
|
|
81
|
+
chart.setNeedHistory(undefined);
|
|
82
|
+
unsub?.();
|
|
83
|
+
},
|
|
84
|
+
candles: () => buf,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export { mergeLive as _mergeLive };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Compact value formatter: large numbers grouped (K/M/B), small numbers to significant digits. */
|
|
2
|
+
export declare function formatValue(v: number): string;
|
|
3
|
+
/** Volume formatter with a leading `$`. */
|
|
4
|
+
export declare function formatVolume(v: number): string;
|
|
5
|
+
/** Axis time label whose granularity follows the bucket `interval` (seconds). */
|
|
6
|
+
export declare function formatTime(t: number, interval: number): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Compact value formatter: large numbers grouped (K/M/B), small numbers to significant digits. */
|
|
2
|
+
export function formatValue(v) {
|
|
3
|
+
if (!isFinite(v))
|
|
4
|
+
return "-";
|
|
5
|
+
const a = Math.abs(v);
|
|
6
|
+
if (a >= 1e9)
|
|
7
|
+
return (v / 1e9).toFixed(2) + "B";
|
|
8
|
+
if (a >= 1e6)
|
|
9
|
+
return (v / 1e6).toFixed(2) + "M";
|
|
10
|
+
if (a >= 1e3)
|
|
11
|
+
return (v / 1e3).toFixed(2) + "K";
|
|
12
|
+
if (a >= 1)
|
|
13
|
+
return v.toFixed(2);
|
|
14
|
+
if (a > 0)
|
|
15
|
+
return v.toPrecision(4);
|
|
16
|
+
return "0";
|
|
17
|
+
}
|
|
18
|
+
/** Volume formatter with a leading `$`. */
|
|
19
|
+
export function formatVolume(v) {
|
|
20
|
+
const a = Math.abs(v);
|
|
21
|
+
if (a >= 1e9)
|
|
22
|
+
return "$" + (v / 1e9).toFixed(1) + "B";
|
|
23
|
+
if (a >= 1e6)
|
|
24
|
+
return "$" + (v / 1e6).toFixed(1) + "M";
|
|
25
|
+
if (a >= 1e3)
|
|
26
|
+
return "$" + (v / 1e3).toFixed(1) + "K";
|
|
27
|
+
return "$" + v.toFixed(0);
|
|
28
|
+
}
|
|
29
|
+
/** Axis time label whose granularity follows the bucket `interval` (seconds). */
|
|
30
|
+
export function formatTime(t, interval) {
|
|
31
|
+
const d = new Date(t * 1000);
|
|
32
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
33
|
+
if (interval < 60)
|
|
34
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
35
|
+
if (interval < 3600)
|
|
36
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
37
|
+
if (interval < 86400)
|
|
38
|
+
return `${d.getMonth() + 1}/${d.getDate()} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
39
|
+
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Candle, Indicator, PriceSource } from "./types";
|
|
2
|
+
/** Default overlay colors, assigned by indicator position when none is given. */
|
|
3
|
+
export declare const INDICATOR_PALETTE: string[];
|
|
4
|
+
/**
|
|
5
|
+
* Simple moving average. Output is aligned to the input (same length); the first
|
|
6
|
+
* `period - 1` entries are null (not enough history). Uses a rolling sum — O(n).
|
|
7
|
+
*/
|
|
8
|
+
export declare function sma(values: number[], period: number): (number | null)[];
|
|
9
|
+
/**
|
|
10
|
+
* Exponential moving average, seeded with the SMA of the first `period` values
|
|
11
|
+
* (the conventional seed). Aligned to input; first `period - 1` entries are null.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ema(values: number[], period: number): (number | null)[];
|
|
14
|
+
/**
|
|
15
|
+
* Weighted moving average — linear weights 1..period (most recent heaviest).
|
|
16
|
+
* Aligned to input; first `period - 1` entries are null.
|
|
17
|
+
*/
|
|
18
|
+
export declare function wma(values: number[], period: number): (number | null)[];
|
|
19
|
+
/**
|
|
20
|
+
* Volume-weighted average price, cumulative from the start of the series. Uses the
|
|
21
|
+
* typical price (h+l+c)/3 weighted by volume. Single line aligned to candles.
|
|
22
|
+
*/
|
|
23
|
+
export declare function vwap(candles: Candle[]): (number | null)[];
|
|
24
|
+
/**
|
|
25
|
+
* Bollinger Bands: a `period` SMA (mid) ± `mult` standard deviations. Returns three
|
|
26
|
+
* series aligned to input — feed them as three overlays for a band.
|
|
27
|
+
*/
|
|
28
|
+
export declare function bollingerBands(values: number[], period?: number, mult?: number): {
|
|
29
|
+
upper: (number | null)[];
|
|
30
|
+
mid: (number | null)[];
|
|
31
|
+
lower: (number | null)[];
|
|
32
|
+
};
|
|
33
|
+
/** Pull a price series (default close) out of candles. */
|
|
34
|
+
export declare function sourceValues(candles: Candle[], source?: PriceSource): number[];
|
|
35
|
+
/** Compute an indicator's value series, aligned to `candles` (null where undefined). */
|
|
36
|
+
export declare function computeIndicator(candles: Candle[], ind: Indicator): (number | null)[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Default overlay colors, assigned by indicator position when none is given. */
|
|
2
|
+
export const INDICATOR_PALETTE = ["#2962ff", "#ff9800", "#ab47bc", "#26a69a", "#ef5350"];
|
|
3
|
+
/**
|
|
4
|
+
* Simple moving average. Output is aligned to the input (same length); the first
|
|
5
|
+
* `period - 1` entries are null (not enough history). Uses a rolling sum — O(n).
|
|
6
|
+
*/
|
|
7
|
+
export function sma(values, period) {
|
|
8
|
+
const out = new Array(values.length).fill(null);
|
|
9
|
+
if (!(period > 0))
|
|
10
|
+
return out;
|
|
11
|
+
let sum = 0;
|
|
12
|
+
for (let i = 0; i < values.length; i++) {
|
|
13
|
+
sum += values[i];
|
|
14
|
+
if (i >= period)
|
|
15
|
+
sum -= values[i - period];
|
|
16
|
+
if (i >= period - 1)
|
|
17
|
+
out[i] = sum / period;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Exponential moving average, seeded with the SMA of the first `period` values
|
|
23
|
+
* (the conventional seed). Aligned to input; first `period - 1` entries are null.
|
|
24
|
+
*/
|
|
25
|
+
export function ema(values, period) {
|
|
26
|
+
const out = new Array(values.length).fill(null);
|
|
27
|
+
if (!(period > 0) || values.length < period)
|
|
28
|
+
return out;
|
|
29
|
+
const k = 2 / (period + 1);
|
|
30
|
+
let seed = 0;
|
|
31
|
+
for (let i = 0; i < period; i++)
|
|
32
|
+
seed += values[i];
|
|
33
|
+
let prev = seed / period;
|
|
34
|
+
out[period - 1] = prev;
|
|
35
|
+
for (let i = period; i < values.length; i++) {
|
|
36
|
+
prev = values[i] * k + prev * (1 - k);
|
|
37
|
+
out[i] = prev;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Weighted moving average — linear weights 1..period (most recent heaviest).
|
|
43
|
+
* Aligned to input; first `period - 1` entries are null.
|
|
44
|
+
*/
|
|
45
|
+
export function wma(values, period) {
|
|
46
|
+
const out = new Array(values.length).fill(null);
|
|
47
|
+
if (!(period > 0))
|
|
48
|
+
return out;
|
|
49
|
+
const denom = (period * (period + 1)) / 2;
|
|
50
|
+
for (let i = period - 1; i < values.length; i++) {
|
|
51
|
+
let s = 0;
|
|
52
|
+
for (let k = 0; k < period; k++)
|
|
53
|
+
s += values[i - period + 1 + k] * (k + 1);
|
|
54
|
+
out[i] = s / denom;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Volume-weighted average price, cumulative from the start of the series. Uses the
|
|
60
|
+
* typical price (h+l+c)/3 weighted by volume. Single line aligned to candles.
|
|
61
|
+
*/
|
|
62
|
+
export function vwap(candles) {
|
|
63
|
+
const out = new Array(candles.length).fill(null);
|
|
64
|
+
let cumPV = 0;
|
|
65
|
+
let cumV = 0;
|
|
66
|
+
for (let i = 0; i < candles.length; i++) {
|
|
67
|
+
const tp = (candles[i].h + candles[i].l + candles[i].c) / 3;
|
|
68
|
+
const v = candles[i].vol || 0;
|
|
69
|
+
cumPV += tp * v;
|
|
70
|
+
cumV += v;
|
|
71
|
+
out[i] = cumV > 0 ? cumPV / cumV : tp;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Bollinger Bands: a `period` SMA (mid) ± `mult` standard deviations. Returns three
|
|
77
|
+
* series aligned to input — feed them as three overlays for a band.
|
|
78
|
+
*/
|
|
79
|
+
export function bollingerBands(values, period = 20, mult = 2) {
|
|
80
|
+
const mid = sma(values, period);
|
|
81
|
+
const upper = new Array(values.length).fill(null);
|
|
82
|
+
const lower = new Array(values.length).fill(null);
|
|
83
|
+
for (let i = period - 1; i < values.length; i++) {
|
|
84
|
+
const m = mid[i];
|
|
85
|
+
if (m == null)
|
|
86
|
+
continue;
|
|
87
|
+
let sq = 0;
|
|
88
|
+
for (let k = i - period + 1; k <= i; k++)
|
|
89
|
+
sq += (values[k] - m) ** 2;
|
|
90
|
+
const sd = Math.sqrt(sq / period);
|
|
91
|
+
upper[i] = m + mult * sd;
|
|
92
|
+
lower[i] = m - mult * sd;
|
|
93
|
+
}
|
|
94
|
+
return { upper, mid, lower };
|
|
95
|
+
}
|
|
96
|
+
const SOURCE_KEY = { open: "o", high: "h", low: "l", close: "c" };
|
|
97
|
+
/** Pull a price series (default close) out of candles. */
|
|
98
|
+
export function sourceValues(candles, source = "close") {
|
|
99
|
+
const key = SOURCE_KEY[source];
|
|
100
|
+
return candles.map((c) => c[key]);
|
|
101
|
+
}
|
|
102
|
+
/** Compute an indicator's value series, aligned to `candles` (null where undefined). */
|
|
103
|
+
export function computeIndicator(candles, ind) {
|
|
104
|
+
if (ind.type === "vwap")
|
|
105
|
+
return vwap(candles);
|
|
106
|
+
const vals = sourceValues(candles, ind.source);
|
|
107
|
+
if (ind.type === "ema")
|
|
108
|
+
return ema(vals, ind.period);
|
|
109
|
+
if (ind.type === "wma")
|
|
110
|
+
return wma(vals, ind.period);
|
|
111
|
+
return sma(vals, ind.period);
|
|
112
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Candle } from "./types";
|
|
2
|
+
import type { ChartFeed } from "./feed";
|
|
3
|
+
/** A raw Hyperliquid candleSnapshot row (numeric fields are strings; `t`/`T` are ms). */
|
|
4
|
+
export interface HlRawCandle {
|
|
5
|
+
t: number;
|
|
6
|
+
T: number;
|
|
7
|
+
s: string;
|
|
8
|
+
i: string;
|
|
9
|
+
o: string;
|
|
10
|
+
c: string;
|
|
11
|
+
h: string;
|
|
12
|
+
l: string;
|
|
13
|
+
v: string;
|
|
14
|
+
n: number;
|
|
15
|
+
}
|
|
16
|
+
/** Map Hyperliquid candleSnapshot rows to chart Candles (ms→seconds, string→number). */
|
|
17
|
+
export declare function mapHlCandles(rows: HlRawCandle[]): Candle[];
|
|
18
|
+
/** Hyperliquid candle interval → bucket seconds (drives the chart's time axis). */
|
|
19
|
+
export declare const HL_INTERVAL_SECONDS: Record<string, number>;
|
|
20
|
+
export interface FetchHlCandlesOptions {
|
|
21
|
+
coin: string;
|
|
22
|
+
/** Hyperliquid interval string (default "1h"). */
|
|
23
|
+
interval?: string;
|
|
24
|
+
/** How far back from now to load (default 24h). */
|
|
25
|
+
lookbackMs?: number;
|
|
26
|
+
testnet?: boolean;
|
|
27
|
+
/** Override the API base (e.g. a CORS proxy). */
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
/** Override "now" (ms) — for tests/determinism. */
|
|
30
|
+
now?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Fetch a Hyperliquid candle window [startTime, endTime] (ms) as chart Candles. */
|
|
33
|
+
export declare function fetchHlCandleWindow(args: {
|
|
34
|
+
coin: string;
|
|
35
|
+
interval: string;
|
|
36
|
+
startTime: number;
|
|
37
|
+
endTime: number;
|
|
38
|
+
testnet?: boolean;
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
}): Promise<Candle[]>;
|
|
41
|
+
/** Fetch recent Hyperliquid candles for a coin/interval as chart Candles (no key). */
|
|
42
|
+
export declare function fetchHyperliquidCandles(opts: FetchHlCandlesOptions): Promise<Candle[]>;
|
|
43
|
+
export interface HyperliquidFeedOptions {
|
|
44
|
+
coin: string;
|
|
45
|
+
/** Hyperliquid interval string (default "1h"). */
|
|
46
|
+
interval?: string;
|
|
47
|
+
testnet?: boolean;
|
|
48
|
+
/** Override the REST base URL. */
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
/** Override the WebSocket URL (default wss://api.hyperliquid[-testnet].xyz/ws). */
|
|
51
|
+
wsUrl?: string;
|
|
52
|
+
/** Override "now" (ms) for the latest-page request — for tests/determinism. */
|
|
53
|
+
now?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A {@link ChartFeed} backed by Hyperliquid: paged candle history over REST and a
|
|
57
|
+
* live candle stream over WebSocket (no key). Pass it to `connectFeed` together with
|
|
58
|
+
* `HL_INTERVAL_SECONDS[interval]` for a realtime, infinitely-scrollable chart.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hyperliquidFeed(opts: HyperliquidFeedOptions): ChartFeed;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Live-data adapters — turn an external market-data source into chart Candles.
|
|
2
|
+
// Dependency-free (uses the global fetch). Hyperliquid's public `info` endpoint
|
|
3
|
+
// needs no key, so this works from the browser or a Worker alike.
|
|
4
|
+
/** Map Hyperliquid candleSnapshot rows to chart Candles (ms→seconds, string→number). */
|
|
5
|
+
export function mapHlCandles(rows) {
|
|
6
|
+
return rows.map((r) => ({
|
|
7
|
+
time: Math.floor(r.t / 1000),
|
|
8
|
+
o: +r.o,
|
|
9
|
+
h: +r.h,
|
|
10
|
+
l: +r.l,
|
|
11
|
+
c: +r.c,
|
|
12
|
+
vol: +r.v,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
/** Hyperliquid candle interval → bucket seconds (drives the chart's time axis). */
|
|
16
|
+
export const HL_INTERVAL_SECONDS = {
|
|
17
|
+
"1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800,
|
|
18
|
+
"1h": 3600, "2h": 7200, "4h": 14400, "8h": 28800, "12h": 43200,
|
|
19
|
+
"1d": 86400, "3d": 259200, "1w": 604800, "1M": 2592000,
|
|
20
|
+
};
|
|
21
|
+
function hlBase(testnet, baseUrl) {
|
|
22
|
+
return baseUrl ?? (testnet ? "https://api.hyperliquid-testnet.xyz" : "https://api.hyperliquid.xyz");
|
|
23
|
+
}
|
|
24
|
+
/** Fetch a Hyperliquid candle window [startTime, endTime] (ms) as chart Candles. */
|
|
25
|
+
export async function fetchHlCandleWindow(args) {
|
|
26
|
+
const res = await fetch(hlBase(args.testnet, args.baseUrl) + "/info", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "content-type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ type: "candleSnapshot", req: { coin: args.coin, interval: args.interval, startTime: args.startTime, endTime: args.endTime } }),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
throw new Error("Hyperliquid candles " + res.status);
|
|
33
|
+
return mapHlCandles((await res.json()));
|
|
34
|
+
}
|
|
35
|
+
/** Fetch recent Hyperliquid candles for a coin/interval as chart Candles (no key). */
|
|
36
|
+
export async function fetchHyperliquidCandles(opts) {
|
|
37
|
+
const { coin, interval = "1h", lookbackMs = 86400000, testnet, baseUrl } = opts;
|
|
38
|
+
const endTime = opts.now ?? Date.now();
|
|
39
|
+
return fetchHlCandleWindow({ coin, interval, startTime: endTime - lookbackMs, endTime, testnet, baseUrl });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A {@link ChartFeed} backed by Hyperliquid: paged candle history over REST and a
|
|
43
|
+
* live candle stream over WebSocket (no key). Pass it to `connectFeed` together with
|
|
44
|
+
* `HL_INTERVAL_SECONDS[interval]` for a realtime, infinitely-scrollable chart.
|
|
45
|
+
*/
|
|
46
|
+
export function hyperliquidFeed(opts) {
|
|
47
|
+
const { coin, interval = "1h", testnet, baseUrl, wsUrl } = opts;
|
|
48
|
+
const secs = HL_INTERVAL_SECONDS[interval] ?? 3600;
|
|
49
|
+
return {
|
|
50
|
+
async loadHistory({ before, limit }) {
|
|
51
|
+
const endTime = before ?? opts.now ?? Date.now();
|
|
52
|
+
const startTime = endTime - secs * 1000 * limit;
|
|
53
|
+
return fetchHlCandleWindow({ coin, interval, startTime, endTime, testnet, baseUrl });
|
|
54
|
+
},
|
|
55
|
+
subscribe(_params, onUpdate) {
|
|
56
|
+
const url = wsUrl ?? (testnet ? "wss://api.hyperliquid-testnet.xyz/ws" : "wss://api.hyperliquid.xyz/ws");
|
|
57
|
+
let closed = false;
|
|
58
|
+
let ws = null;
|
|
59
|
+
let ping;
|
|
60
|
+
const connect = () => {
|
|
61
|
+
if (closed)
|
|
62
|
+
return;
|
|
63
|
+
ws = new WebSocket(url);
|
|
64
|
+
ws.onopen = () => {
|
|
65
|
+
ws?.send(JSON.stringify({ method: "subscribe", subscription: { type: "candle", coin, interval } }));
|
|
66
|
+
ping = setInterval(() => ws?.readyState === 1 && ws.send(JSON.stringify({ method: "ping" })), 30000);
|
|
67
|
+
};
|
|
68
|
+
ws.onmessage = (ev) => {
|
|
69
|
+
try {
|
|
70
|
+
const m = JSON.parse(typeof ev.data === "string" ? ev.data : "");
|
|
71
|
+
if (m.channel === "candle" && m.data)
|
|
72
|
+
onUpdate(mapHlCandles([m.data])[0]);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore malformed frame */
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
ws.onclose = () => {
|
|
79
|
+
if (ping)
|
|
80
|
+
clearInterval(ping);
|
|
81
|
+
if (!closed)
|
|
82
|
+
setTimeout(connect, 2000); // reconnect with backoff
|
|
83
|
+
};
|
|
84
|
+
ws.onerror = () => {
|
|
85
|
+
try {
|
|
86
|
+
ws?.close();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* already closing */
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
connect();
|
|
94
|
+
return () => {
|
|
95
|
+
closed = true;
|
|
96
|
+
if (ping)
|
|
97
|
+
clearInterval(ping);
|
|
98
|
+
try {
|
|
99
|
+
ws?.close();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* already closed */
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Candle, Point, PriceTransform } from "./types";
|
|
2
|
+
/** Apply the denom/flip/supply transform to a single price. Returns 0 for invalid results. */
|
|
3
|
+
export declare function transformPrice(p: number, t?: PriceTransform): number;
|
|
4
|
+
/**
|
|
5
|
+
* Aggregate priced trades into OHLC+volume candles bucketed by `interval` seconds.
|
|
6
|
+
* Points may arrive unsorted; output is ascending by time. Trades whose (transformed)
|
|
7
|
+
* price is non-positive or non-finite are dropped; their volume is dropped with them.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildOHLC(points: Point[], interval: number, transform?: PriceTransform): Candle[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Apply the denom/flip/supply transform to a single price. Returns 0 for invalid results. */
|
|
2
|
+
export function transformPrice(p, t = {}) {
|
|
3
|
+
let v = t.denom === "ETH" && t.ethUsd && t.ethUsd > 0 ? p / t.ethUsd : p;
|
|
4
|
+
if (t.supply && t.supply > 0)
|
|
5
|
+
return v * t.supply; // market cap; flip N/A
|
|
6
|
+
if (t.flip && v > 0)
|
|
7
|
+
v = 1 / v;
|
|
8
|
+
return v > 0 && isFinite(v) ? v : 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Aggregate priced trades into OHLC+volume candles bucketed by `interval` seconds.
|
|
12
|
+
* Points may arrive unsorted; output is ascending by time. Trades whose (transformed)
|
|
13
|
+
* price is non-positive or non-finite are dropped; their volume is dropped with them.
|
|
14
|
+
*/
|
|
15
|
+
export function buildOHLC(points, interval, transform = {}) {
|
|
16
|
+
if (!(interval > 0))
|
|
17
|
+
return [];
|
|
18
|
+
const rows = points
|
|
19
|
+
.map((s) => ({ t: s.t, p: transformPrice(s.p, transform), v: s.v || 0 }))
|
|
20
|
+
.filter((s) => s.p > 0)
|
|
21
|
+
.sort((a, b) => a.t - b.t);
|
|
22
|
+
const buckets = new Map();
|
|
23
|
+
for (const { t, p, v } of rows) {
|
|
24
|
+
const bt = Math.floor(t / interval) * interval;
|
|
25
|
+
const b = buckets.get(bt);
|
|
26
|
+
if (!b)
|
|
27
|
+
buckets.set(bt, { time: bt, o: p, h: p, l: p, c: p, vol: v });
|
|
28
|
+
else {
|
|
29
|
+
b.h = Math.max(b.h, p);
|
|
30
|
+
b.l = Math.min(b.l, p);
|
|
31
|
+
b.c = p;
|
|
32
|
+
b.vol += v;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...buckets.values()].sort((a, b) => a.time - b.time);
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Candle, ChartTheme, ChartType, Indicator } from "./types";
|
|
2
|
+
export interface Viewport {
|
|
3
|
+
/** candles visible. */
|
|
4
|
+
count: number;
|
|
5
|
+
/** candles scrolled off the right edge (0 = latest pinned right). */
|
|
6
|
+
offset: number;
|
|
7
|
+
}
|
|
8
|
+
export interface Pads {
|
|
9
|
+
right: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
top: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RenderInput {
|
|
14
|
+
ctx: CanvasRenderingContext2D;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
candles: Candle[];
|
|
18
|
+
view: Viewport;
|
|
19
|
+
hover: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
} | null;
|
|
23
|
+
interval: number;
|
|
24
|
+
type: ChartType;
|
|
25
|
+
/** manual Y-zoom factor (1 = auto-fit). */
|
|
26
|
+
yZoom: number;
|
|
27
|
+
/** max candle slot width in px. */
|
|
28
|
+
maxBarWidth: number;
|
|
29
|
+
/** volume-panel height in px. */
|
|
30
|
+
volH: number;
|
|
31
|
+
theme: ChartTheme;
|
|
32
|
+
pads: Pads;
|
|
33
|
+
/** moving-average overlays as config — computed here if `overlays` isn't supplied. */
|
|
34
|
+
indicators?: Indicator[];
|
|
35
|
+
/** precomputed overlay value-series (aligned to `candles`); preferred over `indicators`. */
|
|
36
|
+
overlays?: ResolvedOverlay[];
|
|
37
|
+
}
|
|
38
|
+
/** A resolved indicator overlay: a color and a value-series aligned to the candles. */
|
|
39
|
+
export interface ResolvedOverlay {
|
|
40
|
+
color: string;
|
|
41
|
+
values: (number | null)[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pure draw pass: renders grid, axes, the price series (candles or line), a volume
|
|
45
|
+
* panel, the last-price tag and the crosshair + axis labels. Returns the candle under
|
|
46
|
+
* the crosshair (or the last candle when idle, or null when empty) so the host can
|
|
47
|
+
* drive an OHLCV legend. No state is retained between calls.
|
|
48
|
+
*/
|
|
49
|
+
export declare function draw(input: RenderInput): Candle | null;
|