@livo-build/charts 0.2.3 → 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.
|
@@ -32,13 +32,22 @@ export interface PolymarketFeedOptions {
|
|
|
32
32
|
host?: string;
|
|
33
33
|
/** Live poll cadence in ms (default 30s; 0 disables the live poll). */
|
|
34
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;
|
|
35
43
|
/** Override "now" (ms) — for tests/determinism. */
|
|
36
44
|
now?: number;
|
|
37
45
|
}
|
|
38
46
|
/**
|
|
39
47
|
* A {@link ChartFeed} for a Polymarket outcome: paged price history bucketed into OHLC
|
|
40
|
-
* candles, plus a
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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`.
|
|
43
52
|
*/
|
|
44
53
|
export declare function polymarketFeed(opts: PolymarketFeedOptions): ChartFeed;
|
package/dist/core/polymarket.js
CHANGED
|
@@ -41,15 +41,17 @@ export async function fetchPolymarketPriceHistory(opts) {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* A {@link ChartFeed} for a Polymarket outcome: paged price history bucketed into OHLC
|
|
44
|
-
* candles, plus a
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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`.
|
|
47
48
|
*/
|
|
48
49
|
export function polymarketFeed(opts) {
|
|
49
50
|
const secs = opts.bucketSeconds ?? 3600;
|
|
50
51
|
const host = opts.host ?? POLYMARKET_CLOB;
|
|
51
52
|
const tokenId = opts.tokenId;
|
|
52
53
|
const fidelity = Math.max(1, Math.round(secs / 60));
|
|
54
|
+
const ES = globalThis.EventSource;
|
|
53
55
|
return {
|
|
54
56
|
async loadHistory({ before, limit }) {
|
|
55
57
|
const endTs = Math.floor((before ?? opts.now ?? Date.now()) / 1000);
|
|
@@ -58,35 +60,114 @@ export function polymarketFeed(opts) {
|
|
|
58
60
|
return buildOHLC(points, secs);
|
|
59
61
|
},
|
|
60
62
|
subscribe(_params, onUpdate) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
};
|
|
89
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();
|
|
90
99
|
},
|
|
91
100
|
};
|
|
92
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
|
+
}
|
|
@@ -6,8 +6,13 @@ export interface PolymarketChartProps {
|
|
|
6
6
|
bucketSeconds?: number;
|
|
7
7
|
/** Override the CLOB host. */
|
|
8
8
|
host?: string;
|
|
9
|
-
/** Live poll cadence in ms (default 30s; 0 = fetch once). */
|
|
9
|
+
/** Live poll cadence in ms (default 30s; 0 = fetch once). Ignored when `liveUrl` is set. */
|
|
10
10
|
refetchMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Optional Livo indexer base (e.g. "https://polymarket.livo.build") — upgrades the live
|
|
13
|
+
* candle from the 30s poll to INSTANT SSE updates. History still loads from the CLOB.
|
|
14
|
+
*/
|
|
15
|
+
liveUrl?: string;
|
|
11
16
|
/** Candles per history page; older pages load as you scroll left (default 500). */
|
|
12
17
|
pageSize?: number;
|
|
13
18
|
height?: number;
|
|
@@ -37,4 +42,4 @@ export interface PolymarketChartProps {
|
|
|
37
42
|
* <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
|
|
38
43
|
* ```
|
|
39
44
|
*/
|
|
40
|
-
export declare function PolymarketChart({ tokenId, bucketSeconds, host, refetchMs, pageSize, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, theme, toolbar, fitContent, label, priceFormat, }: PolymarketChartProps): import("react").JSX.Element;
|
|
45
|
+
export declare function PolymarketChart({ tokenId, bucketSeconds, host, refetchMs, liveUrl, pageSize, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, theme, toolbar, fitContent, label, priceFormat, }: PolymarketChartProps): import("react").JSX.Element;
|
|
@@ -16,7 +16,7 @@ const pct = (v) => `${(v * 100).toFixed(1)}%`;
|
|
|
16
16
|
* <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
-
export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs, pageSize = 500, height = 420, chartType = "line", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = false, volumeProfile: volumeProfileProp, theme, toolbar = true, fitContent = true, label, priceFormat = pct, }) {
|
|
19
|
+
export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs, liveUrl, pageSize = 500, height = 420, chartType = "line", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = false, volumeProfile: volumeProfileProp, theme, toolbar = true, fitContent = true, label, priceFormat = pct, }) {
|
|
20
20
|
const [secs, setSecs] = useState(bucketSeconds);
|
|
21
21
|
const [type, setType] = useState(chartType);
|
|
22
22
|
const [log, setLog] = useState(false);
|
|
@@ -70,7 +70,7 @@ export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs
|
|
|
70
70
|
return;
|
|
71
71
|
setLoading(true);
|
|
72
72
|
setError(null);
|
|
73
|
-
const feed = polymarketFeed({ tokenId, bucketSeconds: secs, host, refetchMs });
|
|
73
|
+
const feed = polymarketFeed({ tokenId, bucketSeconds: secs, host, refetchMs, liveUrl });
|
|
74
74
|
const conn = connectFeed(chart.current, feed, {
|
|
75
75
|
interval: secs,
|
|
76
76
|
pageSize,
|
|
@@ -85,7 +85,7 @@ export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs
|
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
87
|
return () => conn.disconnect();
|
|
88
|
-
}, [tokenId, secs, host, refetchMs, pageSize]);
|
|
88
|
+
}, [tokenId, secs, host, refetchMs, liveUrl, pageSize]);
|
|
89
89
|
const info = legend ?? last;
|
|
90
90
|
const lc = info && info.c >= info.o ? th.up : th.down;
|
|
91
91
|
const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livo-build/charts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "livo-charts — a lightweight, dependency-free canvas charting library (candlesticks/line/baseline/Heikin-Ashi, zoom/pan, crosshair, MA/RSI/MACD/Stochastic/ATR indicators, volume profile, drawing tools, and live Hyperliquid/Polymarket/Signal-Radar feeds) with a framework-agnostic core and a React wrapper.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|