@luxalgo/vela 0.5.0
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/LICENSE +201 -0
- package/NOTICE +28 -0
- package/README.md +135 -0
- package/dist/DataProvider-DKNDHpNv.d.cts +134 -0
- package/dist/DataProvider-Dzd-Erlk.d.ts +134 -0
- package/dist/chunk-7UFX5ZIG.js +5976 -0
- package/dist/chunk-GYD2THPV.js +252 -0
- package/dist/chunk-KCCZNKH7.js +19137 -0
- package/dist/chunk-KM6LHB3Y.js +76 -0
- package/dist/chunk-OVQKKXLZ.js +3978 -0
- package/dist/chunk-Q3XQHLIH.js +7 -0
- package/dist/chunk-RHIDOUFL.js +698 -0
- package/dist/contributions-hX3EUyjG.d.ts +1528 -0
- package/dist/contributions-o1GRKPI_.d.cts +1528 -0
- package/dist/history-Dzxz-MQj.d.ts +298 -0
- package/dist/history-LJkz4-sS.d.cts +298 -0
- package/dist/icons-BZYbJXSV.d.cts +10 -0
- package/dist/icons-BZYbJXSV.d.ts +10 -0
- package/dist/index.cjs +25294 -0
- package/dist/index.d.cts +1189 -0
- package/dist/index.d.ts +1189 -0
- package/dist/index.js +5 -0
- package/dist/keymap-CGOz5F5f.d.cts +64 -0
- package/dist/keymap-CGOz5F5f.d.ts +64 -0
- package/dist/options-Q-576hIi.d.cts +1545 -0
- package/dist/options-Q-576hIi.d.ts +1545 -0
- package/dist/plugin-D94muTV-.d.cts +405 -0
- package/dist/plugin-aGUD1epn.d.ts +405 -0
- package/dist/plugin.cjs +5967 -0
- package/dist/plugin.d.cts +7 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +3 -0
- package/dist/providers/binance.cjs +390 -0
- package/dist/providers/binance.d.cts +62 -0
- package/dist/providers/binance.d.ts +62 -0
- package/dist/providers/binance.js +388 -0
- package/dist/providers/coinbase.cjs +462 -0
- package/dist/providers/coinbase.d.cts +59 -0
- package/dist/providers/coinbase.d.ts +59 -0
- package/dist/providers/coinbase.js +460 -0
- package/dist/providers/hyperliquid.cjs +361 -0
- package/dist/providers/hyperliquid.d.cts +54 -0
- package/dist/providers/hyperliquid.d.ts +54 -0
- package/dist/providers/hyperliquid.js +359 -0
- package/dist/side-panel-CT9ZwIGz.d.cts +63 -0
- package/dist/side-panel-CT9ZwIGz.d.ts +63 -0
- package/dist/ui.cjs +1030 -0
- package/dist/ui.d.cts +188 -0
- package/dist/ui.d.ts +188 -0
- package/dist/ui.js +3 -0
- package/dist/vela.global.js +26509 -0
- package/dist/vela.global.min.js +230 -0
- package/dist/widget.cjs +31178 -0
- package/dist/widget.d.cts +799 -0
- package/dist/widget.d.ts +799 -0
- package/dist/widget.js +1060 -0
- package/dist/workspace.cjs +31692 -0
- package/dist/workspace.d.cts +579 -0
- package/dist/workspace.d.ts +579 -0
- package/dist/workspace.js +1694 -0
- package/package.json +95 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/data/providers/coinbase/RequestGate.ts
|
|
4
|
+
var REAL_CLOCK = {
|
|
5
|
+
now: () => Date.now(),
|
|
6
|
+
delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
};
|
|
8
|
+
var RequestGate = class {
|
|
9
|
+
constructor(maxConcurrent, minIntervalMs, clock = REAL_CLOCK) {
|
|
10
|
+
this.maxConcurrent = maxConcurrent;
|
|
11
|
+
this.minIntervalMs = minIntervalMs;
|
|
12
|
+
this.clock = clock;
|
|
13
|
+
this.active = 0;
|
|
14
|
+
this.waiters = [];
|
|
15
|
+
/** Earliest time the next request may START (min-spacing reservation). */
|
|
16
|
+
this.nextStartAt = 0;
|
|
17
|
+
/** Backoff deadline set by a 429 — new starts wait until then. */
|
|
18
|
+
this.pausedUntil = 0;
|
|
19
|
+
}
|
|
20
|
+
/** Acquire a slot, wait for the spacing/pause turn, run `fn`, release. */
|
|
21
|
+
async run(fn) {
|
|
22
|
+
await this.acquire();
|
|
23
|
+
try {
|
|
24
|
+
await this.waitForTurn();
|
|
25
|
+
return await fn();
|
|
26
|
+
} finally {
|
|
27
|
+
this.release();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Pause new starts for `ms` (a 429 Retry-After / exponential backoff). */
|
|
31
|
+
pauseFor(ms) {
|
|
32
|
+
if (ms > 0) this.pausedUntil = Math.max(this.pausedUntil, this.clock.now() + ms);
|
|
33
|
+
}
|
|
34
|
+
/** Block until the spacing window opens and any pause has elapsed, then reserve the next slot. */
|
|
35
|
+
async waitForTurn() {
|
|
36
|
+
for (; ; ) {
|
|
37
|
+
const now = this.clock.now();
|
|
38
|
+
const wait = Math.max(this.pausedUntil - now, this.nextStartAt - now);
|
|
39
|
+
if (wait <= 0) break;
|
|
40
|
+
await this.clock.delay(wait);
|
|
41
|
+
}
|
|
42
|
+
this.nextStartAt = Math.max(this.clock.now(), this.nextStartAt) + this.minIntervalMs;
|
|
43
|
+
}
|
|
44
|
+
acquire() {
|
|
45
|
+
if (this.active < this.maxConcurrent) {
|
|
46
|
+
this.active += 1;
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
this.waiters.push(() => {
|
|
51
|
+
this.active += 1;
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
release() {
|
|
57
|
+
this.active -= 1;
|
|
58
|
+
const next = this.waiters.shift();
|
|
59
|
+
if (next) next();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/data/providers/coinbase/CoinbaseProvider.ts
|
|
64
|
+
var REST_BASE = "https://api.exchange.coinbase.com";
|
|
65
|
+
var WS_URL = "wss://ws-feed.exchange.coinbase.com";
|
|
66
|
+
var REQ_HEADERS = { Accept: "application/json" };
|
|
67
|
+
var STREAM_STALL_MS = 15e3;
|
|
68
|
+
var STREAM_RECONNECT_MS = 2e3;
|
|
69
|
+
var LIVE_RESEED_MS = 5e3;
|
|
70
|
+
var MAX_CANDLES_PER_REQ = 300;
|
|
71
|
+
var REST_CONCURRENCY = 4;
|
|
72
|
+
var REST_MIN_INTERVAL_MS = 120;
|
|
73
|
+
var REQUEST_MAX_RETRIES = 4;
|
|
74
|
+
var BACKOFF_BASE_MS = 600;
|
|
75
|
+
var BACKOFF_JITTER_MS = 400;
|
|
76
|
+
function retryAfterMs(res, fallbackMs) {
|
|
77
|
+
const sec = Number(res.headers.get("retry-after"));
|
|
78
|
+
return Number.isFinite(sec) && sec > 0 ? sec * 1e3 : fallbackMs;
|
|
79
|
+
}
|
|
80
|
+
var TF_TO_GRAN = {
|
|
81
|
+
"1": 60,
|
|
82
|
+
"5": 300,
|
|
83
|
+
"15": 900,
|
|
84
|
+
"60": 3600,
|
|
85
|
+
"360": 21600,
|
|
86
|
+
D: 86400
|
|
87
|
+
};
|
|
88
|
+
var TF_NORMALIZE = {
|
|
89
|
+
"1m": "1",
|
|
90
|
+
"3m": "3",
|
|
91
|
+
"5m": "5",
|
|
92
|
+
"15m": "15",
|
|
93
|
+
"30m": "30",
|
|
94
|
+
"45m": "45",
|
|
95
|
+
"1h": "60",
|
|
96
|
+
"2h": "120",
|
|
97
|
+
"3h": "180",
|
|
98
|
+
"4h": "240",
|
|
99
|
+
"6h": "360",
|
|
100
|
+
"8h": "480",
|
|
101
|
+
"12h": "720",
|
|
102
|
+
"1d": "D",
|
|
103
|
+
"1w": "W",
|
|
104
|
+
"1mo": "M",
|
|
105
|
+
"1D": "D",
|
|
106
|
+
"1W": "W",
|
|
107
|
+
"4H": "240",
|
|
108
|
+
D: "D",
|
|
109
|
+
W: "W",
|
|
110
|
+
M: "M"
|
|
111
|
+
};
|
|
112
|
+
var NATIVE_MINUTES = [1, 5, 15, 60, 360, 1440];
|
|
113
|
+
var MIN_TO_GRAN = { 1: 60, 5: 300, 15: 900, 60: 3600, 360: 21600, 1440: 86400 };
|
|
114
|
+
var SUPPORTED_TIMEFRAMES = ["1", "3", "5", "15", "30", "45", "60", "120", "180", "240", "360", "480", "720", "D", "W", "M"];
|
|
115
|
+
var MS_PER_DAY = 864e5;
|
|
116
|
+
function normalizeTf(tf) {
|
|
117
|
+
return TF_NORMALIZE[tf] ?? TF_NORMALIZE[tf.toLowerCase()] ?? tf;
|
|
118
|
+
}
|
|
119
|
+
function parseProductId(ticker) {
|
|
120
|
+
return ticker.trim().toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
function candleRowToOHLCV(r) {
|
|
123
|
+
return { time: Number(r[0]) * 1e3, open: Number(r[3]), high: Number(r[2]), low: Number(r[1]), close: Number(r[4]), volume: Number(r[5]) };
|
|
124
|
+
}
|
|
125
|
+
function dedupeSorted(bars) {
|
|
126
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const b of bars) byTime.set(b.time, b);
|
|
128
|
+
return [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
129
|
+
}
|
|
130
|
+
function aggregate(sub, bucketMs) {
|
|
131
|
+
return aggregateBy(sub, (t) => Math.floor(t / bucketMs) * bucketMs);
|
|
132
|
+
}
|
|
133
|
+
function aggregateCalendar(sub, unit) {
|
|
134
|
+
return aggregateBy(sub, unit === "W" ? weekStartUTC : monthStartUTC);
|
|
135
|
+
}
|
|
136
|
+
function aggregateBy(sub, keyMs) {
|
|
137
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const b of sub) {
|
|
139
|
+
const key = keyMs(b.time);
|
|
140
|
+
const cur = buckets.get(key);
|
|
141
|
+
if (!cur) {
|
|
142
|
+
buckets.set(key, { time: key, open: b.open, high: b.high, low: b.low, close: b.close, volume: b.volume ?? 0 });
|
|
143
|
+
} else {
|
|
144
|
+
cur.high = Math.max(cur.high, b.high);
|
|
145
|
+
cur.low = Math.min(cur.low, b.low);
|
|
146
|
+
cur.close = b.close;
|
|
147
|
+
cur.volume = (cur.volume ?? 0) + (b.volume ?? 0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [...buckets.values()].sort((a, b) => a.time - b.time);
|
|
151
|
+
}
|
|
152
|
+
function weekStartUTC(ms) {
|
|
153
|
+
const d = new Date(ms);
|
|
154
|
+
const sinceMonday = (d.getUTCDay() + 6) % 7;
|
|
155
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - sinceMonday);
|
|
156
|
+
}
|
|
157
|
+
function monthStartUTC(ms) {
|
|
158
|
+
const d = new Date(ms);
|
|
159
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
160
|
+
}
|
|
161
|
+
function selectSubTf(targetMin) {
|
|
162
|
+
return NATIVE_MINUTES.filter((m) => m < targetMin && targetMin % m === 0).sort((a, b) => b - a)[0] ?? null;
|
|
163
|
+
}
|
|
164
|
+
function clampLimit(bars, limit) {
|
|
165
|
+
return limit != null && bars.length > limit ? bars.slice(-limit) : bars;
|
|
166
|
+
}
|
|
167
|
+
var CoinbaseProvider = class {
|
|
168
|
+
constructor() {
|
|
169
|
+
/** Cached symbol enumeration (the products list is large; fetch once). */
|
|
170
|
+
this.symbolsPromise = null;
|
|
171
|
+
/** Shared request gate: caps concurrency, spaces request starts, honors 429 backoff. */
|
|
172
|
+
this.gate = new RequestGate(REST_CONCURRENCY, REST_MIN_INTERVAL_MS);
|
|
173
|
+
}
|
|
174
|
+
info() {
|
|
175
|
+
return {
|
|
176
|
+
name: "coinbase",
|
|
177
|
+
displayName: "Coinbase",
|
|
178
|
+
requiresApiKey: false,
|
|
179
|
+
supportedTimeframes: SUPPORTED_TIMEFRAMES,
|
|
180
|
+
capabilities: { enumerate: true, stream: true, symbolInfo: true }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async getBars(ticker, timeframe, range) {
|
|
184
|
+
try {
|
|
185
|
+
const product = parseProductId(ticker);
|
|
186
|
+
const tf = normalizeTf(timeframe);
|
|
187
|
+
const gran = TF_TO_GRAN[tf];
|
|
188
|
+
if (gran) return dedupeSorted(await this.fetchCandles(product, gran, range));
|
|
189
|
+
if (tf === "W" || tf === "M") {
|
|
190
|
+
const span = tf === "W" ? 7 * MS_PER_DAY : 31 * MS_PER_DAY;
|
|
191
|
+
const subRange2 = range.from != null ? range : { ...range, limit: range.limit != null ? Math.ceil(range.limit * span / MS_PER_DAY) + 31 : void 0 };
|
|
192
|
+
const sub2 = await this.fetchCandles(product, 86400, subRange2);
|
|
193
|
+
return clampLimit(aggregateCalendar(sub2, tf), range.limit);
|
|
194
|
+
}
|
|
195
|
+
const targetMin = /^\d+$/.test(tf) ? parseInt(tf, 10) : null;
|
|
196
|
+
const subMin = targetMin != null ? selectSubTf(targetMin) : null;
|
|
197
|
+
if (targetMin == null || subMin == null) {
|
|
198
|
+
console.warn(`[vela] Coinbase: timeframe "${timeframe}" is not supported and cannot be aggregated.`);
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const ratio = targetMin / subMin;
|
|
202
|
+
const subRange = { ...range, limit: range.limit != null ? range.limit * ratio + ratio : void 0 };
|
|
203
|
+
const sub = await this.fetchCandles(product, MIN_TO_GRAN[subMin], subRange);
|
|
204
|
+
return clampLimit(aggregate(sub, targetMin * 6e4), range.limit);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.warn(`[vela] Coinbase: failed to fetch ${ticker} ${timeframe} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async getSymbolInfo(ticker) {
|
|
211
|
+
const product = parseProductId(ticker);
|
|
212
|
+
const p = await this.json(`${REST_BASE}/products/${product}`).catch(() => null);
|
|
213
|
+
if (!p || !p.id) return void 0;
|
|
214
|
+
const tickSize = p.quote_increment != null ? parseFloat(p.quote_increment) : 0.01;
|
|
215
|
+
const mintick = tickSize > 0 ? tickSize : 0.01;
|
|
216
|
+
return {
|
|
217
|
+
ticker,
|
|
218
|
+
// keep the original ticker (as Pine Script expects)
|
|
219
|
+
tickerid: `COINBASE:${ticker}`,
|
|
220
|
+
prefix: "COINBASE",
|
|
221
|
+
description: `${p.base_currency} / ${p.quote_currency}`,
|
|
222
|
+
type: "crypto",
|
|
223
|
+
basecurrency: p.base_currency,
|
|
224
|
+
currency: p.quote_currency,
|
|
225
|
+
mintick,
|
|
226
|
+
pricescale: Math.round(1 / mintick),
|
|
227
|
+
timezone: "Etc/UTC",
|
|
228
|
+
session: "24x7"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
listSymbols() {
|
|
232
|
+
if (!this.symbolsPromise) {
|
|
233
|
+
this.symbolsPromise = this.fetchProducts().then((products) => products.filter((p) => p.status === "online" && !p.trading_disabled).map((p) => ({ ticker: p.id, description: `${p.base_currency} / ${p.quote_currency}`, type: "crypto" }))).catch(() => []);
|
|
234
|
+
}
|
|
235
|
+
return this.symbolsPromise;
|
|
236
|
+
}
|
|
237
|
+
subscribe(ticker, timeframe, onBar) {
|
|
238
|
+
const gran = TF_TO_GRAN[normalizeTf(timeframe)];
|
|
239
|
+
if (gran && typeof WebSocket !== "undefined") return this.streamTicker(ticker, timeframe, gran, onBar);
|
|
240
|
+
return this.pollBars(ticker, timeframe, onBar);
|
|
241
|
+
}
|
|
242
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
243
|
+
async fetchProducts() {
|
|
244
|
+
const data = await this.json(`${REST_BASE}/products`);
|
|
245
|
+
return Array.isArray(data) ? data : [];
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Fetch candles for a count (most-recent `limit`) or a `[from, to]` range, paginating past the
|
|
249
|
+
* 300-bucket cap. Returns ascending OHLCV (newest-first rows from the API are sorted on the way out).
|
|
250
|
+
*/
|
|
251
|
+
async fetchCandles(product, granSec, range) {
|
|
252
|
+
if (range.from != null) return this.paginateForward(product, granSec, range.from, range.to ?? Date.now(), range.limit);
|
|
253
|
+
return this.paginateBackward(product, granSec, range.limit ?? 500, range.to ?? Date.now());
|
|
254
|
+
}
|
|
255
|
+
/** Assemble the most-recent `limit` bars, walking backward in ≤300-bucket windows. */
|
|
256
|
+
async paginateBackward(product, granSec, limit, endMs) {
|
|
257
|
+
const granMs = granSec * 1e3;
|
|
258
|
+
let out = [];
|
|
259
|
+
let remaining = limit;
|
|
260
|
+
let cursorEnd = endMs;
|
|
261
|
+
let guard = Math.ceil(limit / MAX_CANDLES_PER_REQ) + 4;
|
|
262
|
+
while (remaining > 0 && guard-- > 0) {
|
|
263
|
+
const size = Math.min(remaining, MAX_CANDLES_PER_REQ);
|
|
264
|
+
const startMs = cursorEnd - size * granMs;
|
|
265
|
+
const rows = await this.candlesChunk(product, granSec, startMs, cursorEnd);
|
|
266
|
+
if (rows.length === 0) break;
|
|
267
|
+
out = rows.concat(out);
|
|
268
|
+
remaining -= rows.length;
|
|
269
|
+
cursorEnd = rows[0].time - 1;
|
|
270
|
+
if (rows.length < size) break;
|
|
271
|
+
}
|
|
272
|
+
return clampLimit(dedupeSorted(out), limit);
|
|
273
|
+
}
|
|
274
|
+
/** Walk `[from, to]` forward in ≤300-bucket windows (ranged/tail fetches). */
|
|
275
|
+
async paginateForward(product, granSec, fromMs, toMs, limit) {
|
|
276
|
+
const granMs = granSec * 1e3;
|
|
277
|
+
const out = [];
|
|
278
|
+
let cursor = fromMs;
|
|
279
|
+
let guard = Math.ceil((toMs - fromMs) / (MAX_CANDLES_PER_REQ * granMs)) + 4;
|
|
280
|
+
while (cursor < toMs && guard-- > 0) {
|
|
281
|
+
const windowEnd = Math.min(cursor + MAX_CANDLES_PER_REQ * granMs, toMs);
|
|
282
|
+
const rows = await this.candlesChunk(product, granSec, cursor, windowEnd);
|
|
283
|
+
if (rows.length > 0) out.push(...rows);
|
|
284
|
+
cursor = windowEnd;
|
|
285
|
+
if (limit != null && out.length >= limit) break;
|
|
286
|
+
}
|
|
287
|
+
const sorted = dedupeSorted(out);
|
|
288
|
+
return limit != null ? sorted.slice(0, limit) : sorted;
|
|
289
|
+
}
|
|
290
|
+
/** One candles request over `[startMs, endMs]` → ascending OHLCV (the API returns newest-first). */
|
|
291
|
+
async candlesChunk(product, granSec, startMs, endMs) {
|
|
292
|
+
const url = new URL(`${REST_BASE}/products/${product}/candles`);
|
|
293
|
+
url.searchParams.set("granularity", String(granSec));
|
|
294
|
+
url.searchParams.set("start", String(Math.floor(startMs / 1e3)));
|
|
295
|
+
url.searchParams.set("end", String(Math.floor(endMs / 1e3)));
|
|
296
|
+
const data = await this.json(url.toString());
|
|
297
|
+
if (!Array.isArray(data) || data.length > 0 && !Array.isArray(data[0])) return [];
|
|
298
|
+
return data.map(candleRowToOHLCV).sort((a, b) => a.time - b.time);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Build a forming candle from the Coinbase `ticker` channel (Coinbase has no native kline stream):
|
|
302
|
+
* the stream supplies a smooth live price/high/low, while a periodic REST re-seed fixes the
|
|
303
|
+
* authoritative open + volume and corrects any drift. A stall watchdog falls back to polling if the
|
|
304
|
+
* socket opens but never delivers a price; reconnects on an unexpected close until unsubscribed.
|
|
305
|
+
*/
|
|
306
|
+
streamTicker(ticker, timeframe, granSec, onBar) {
|
|
307
|
+
const product = parseProductId(ticker);
|
|
308
|
+
const granMs = granSec * 1e3;
|
|
309
|
+
const align = (ms) => Math.floor(ms / granMs) * granMs;
|
|
310
|
+
let closed = false;
|
|
311
|
+
let ws = null;
|
|
312
|
+
let reconnect = null;
|
|
313
|
+
let stall = null;
|
|
314
|
+
let reseedTimer = null;
|
|
315
|
+
let polling = null;
|
|
316
|
+
let current = null;
|
|
317
|
+
const clearStall = () => {
|
|
318
|
+
if (stall) {
|
|
319
|
+
clearTimeout(stall);
|
|
320
|
+
stall = null;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const clearReseed = () => {
|
|
324
|
+
if (reseedTimer) {
|
|
325
|
+
clearInterval(reseedTimer);
|
|
326
|
+
reseedTimer = null;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const emit = () => {
|
|
330
|
+
if (current) onBar({ ...current });
|
|
331
|
+
};
|
|
332
|
+
const reseed = async (emitClosed) => {
|
|
333
|
+
try {
|
|
334
|
+
const bars = await this.getBars(ticker, timeframe, { limit: 2 });
|
|
335
|
+
if (closed || bars.length === 0) return;
|
|
336
|
+
if (emitClosed && bars.length >= 2) ;
|
|
337
|
+
const last = bars[bars.length - 1];
|
|
338
|
+
current = current && current.time === last.time ? { ...last, high: Math.max(last.high, current.high), low: Math.min(last.low, current.low), close: current.close } : { ...last };
|
|
339
|
+
emit();
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const onTick = (priceStr, tMs) => {
|
|
344
|
+
const price = Number(priceStr);
|
|
345
|
+
if (!Number.isFinite(price)) return;
|
|
346
|
+
const barStart = align(tMs);
|
|
347
|
+
if (!current || barStart > current.time) {
|
|
348
|
+
current = { time: barStart, open: price, high: price, low: price, close: price, volume: 0 };
|
|
349
|
+
emit();
|
|
350
|
+
void reseed(false);
|
|
351
|
+
} else if (barStart === current.time) {
|
|
352
|
+
current = { ...current, close: price, high: Math.max(current.high, price), low: Math.min(current.low, price) };
|
|
353
|
+
emit();
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const fallToPolling = () => {
|
|
357
|
+
if (closed || polling) return;
|
|
358
|
+
clearStall();
|
|
359
|
+
clearReseed();
|
|
360
|
+
if (reconnect) {
|
|
361
|
+
clearTimeout(reconnect);
|
|
362
|
+
reconnect = null;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
ws?.close();
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
ws = null;
|
|
369
|
+
console.warn(`[vela] Coinbase: ${ticker} ${timeframe} stream delivered no data; falling back to polling.`);
|
|
370
|
+
polling = this.pollBars(ticker, timeframe, onBar);
|
|
371
|
+
};
|
|
372
|
+
const open = () => {
|
|
373
|
+
if (closed || polling) return;
|
|
374
|
+
ws = new WebSocket(WS_URL);
|
|
375
|
+
stall = setTimeout(fallToPolling, STREAM_STALL_MS);
|
|
376
|
+
ws.onopen = () => {
|
|
377
|
+
try {
|
|
378
|
+
ws?.send(JSON.stringify({ type: "subscribe", product_ids: [product], channels: ["ticker"] }));
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
void reseed(false);
|
|
382
|
+
reseedTimer = setInterval(() => void reseed(false), LIVE_RESEED_MS);
|
|
383
|
+
};
|
|
384
|
+
ws.onmessage = (ev) => {
|
|
385
|
+
if (closed || polling) return;
|
|
386
|
+
try {
|
|
387
|
+
const msg = JSON.parse(typeof ev.data === "string" ? ev.data : "");
|
|
388
|
+
if (msg.type === "ticker" && msg.product_id === product && msg.price != null) {
|
|
389
|
+
clearStall();
|
|
390
|
+
onTick(msg.price, msg.time ? Date.parse(msg.time) : Date.now());
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
ws.onclose = () => {
|
|
396
|
+
clearReseed();
|
|
397
|
+
if (!closed && !polling) reconnect = setTimeout(open, STREAM_RECONNECT_MS);
|
|
398
|
+
};
|
|
399
|
+
ws.onerror = () => {
|
|
400
|
+
try {
|
|
401
|
+
ws?.close();
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
open();
|
|
407
|
+
return () => {
|
|
408
|
+
closed = true;
|
|
409
|
+
clearStall();
|
|
410
|
+
clearReseed();
|
|
411
|
+
if (reconnect) clearTimeout(reconnect);
|
|
412
|
+
polling?.();
|
|
413
|
+
try {
|
|
414
|
+
ws?.close();
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** Poll the forming candle (aggregated/W/M timeframes, or environments without WebSocket). */
|
|
420
|
+
pollBars(ticker, timeframe, onBar) {
|
|
421
|
+
let stopped = false;
|
|
422
|
+
let timer = null;
|
|
423
|
+
const poll = async () => {
|
|
424
|
+
if (stopped) return;
|
|
425
|
+
try {
|
|
426
|
+
const bars = await this.getBars(ticker, timeframe, { limit: 2 });
|
|
427
|
+
for (const b of bars) onBar(b);
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
if (!stopped) timer = setTimeout(() => void poll(), 3e3);
|
|
431
|
+
};
|
|
432
|
+
timer = setTimeout(() => void poll(), 3e3);
|
|
433
|
+
return () => {
|
|
434
|
+
stopped = true;
|
|
435
|
+
if (timer) clearTimeout(timer);
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** GET → parsed JSON, through the rate gate with 429 backoff. */
|
|
439
|
+
async json(url) {
|
|
440
|
+
const res = await this.request(url);
|
|
441
|
+
return res.json();
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Issue one GET through the shared {@link gate} (concurrency + spacing), retrying after a 429
|
|
445
|
+
* (honoring `Retry-After`, else exponential backoff + jitter). Returns the `Response` so callers
|
|
446
|
+
* can read pagination headers (`cb-after`) before consuming the body.
|
|
447
|
+
*/
|
|
448
|
+
async request(url) {
|
|
449
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
450
|
+
const res = await this.gate.run(() => fetch(url, { headers: REQ_HEADERS }));
|
|
451
|
+
if (res.status === 429 && attempt < REQUEST_MAX_RETRIES) {
|
|
452
|
+
const backoff = BACKOFF_BASE_MS * 2 ** attempt + Math.random() * BACKOFF_JITTER_MS;
|
|
453
|
+
this.gate.pauseFor(Math.max(retryAfterMs(res, 0), backoff));
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (!res.ok) throw new Error(`Coinbase HTTP ${res.status} for ${url}`);
|
|
457
|
+
return res;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
exports.CoinbaseProvider = CoinbaseProvider;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { O as OHLCV, U as Unsubscribe } from '../options-Q-576hIi.cjs';
|
|
2
|
+
import { D as DataProvider, P as ProviderInfo, B as BarRange, S as SymbolInfo, a as SymbolDescriptor } from '../DataProvider-DKNDHpNv.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Coinbase market-data provider, built from scratch on the public Exchange REST + WebSocket APIs
|
|
6
|
+
* — no third-party SDK, no API key. Serves spot products (`BTC-USD`, `ETH-EUR`, …), paginates past the
|
|
7
|
+
* 300-candle cap, aggregates timeframes Coinbase doesn't serve natively (e.g. `30`, `4h`) and
|
|
8
|
+
* folds `W`/`M` from daily. Live ticks build a forming candle from the `ticker` stream (Coinbase
|
|
9
|
+
* has no native kline stream), with a poll fallback.
|
|
10
|
+
*
|
|
11
|
+
* Trades: Coinbase trades cursor-walk back from the live tip (the `cb-after` header) with no
|
|
12
|
+
* time-seek, so trade-derived depth is `'recent'` — live and recent windows reconstruct; older bars
|
|
13
|
+
* are served empty rather than walked unbounded.
|
|
14
|
+
*
|
|
15
|
+
* import { CoinbaseProvider } from 'vela/providers/coinbase';
|
|
16
|
+
* chart.data.registerProvider('coinbase', new CoinbaseProvider());
|
|
17
|
+
*/
|
|
18
|
+
declare class CoinbaseProvider implements DataProvider {
|
|
19
|
+
/** Cached symbol enumeration (the products list is large; fetch once). */
|
|
20
|
+
private symbolsPromise;
|
|
21
|
+
/** Shared request gate: caps concurrency, spaces request starts, honors 429 backoff. */
|
|
22
|
+
private readonly gate;
|
|
23
|
+
info(): ProviderInfo;
|
|
24
|
+
getBars(ticker: string, timeframe: string, range: BarRange): Promise<OHLCV[]>;
|
|
25
|
+
getSymbolInfo(ticker: string): Promise<SymbolInfo | undefined>;
|
|
26
|
+
listSymbols(): Promise<SymbolDescriptor[]>;
|
|
27
|
+
subscribe(ticker: string, timeframe: string, onBar: (bar: OHLCV) => void): Unsubscribe;
|
|
28
|
+
private fetchProducts;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch candles for a count (most-recent `limit`) or a `[from, to]` range, paginating past the
|
|
31
|
+
* 300-bucket cap. Returns ascending OHLCV (newest-first rows from the API are sorted on the way out).
|
|
32
|
+
*/
|
|
33
|
+
private fetchCandles;
|
|
34
|
+
/** Assemble the most-recent `limit` bars, walking backward in ≤300-bucket windows. */
|
|
35
|
+
private paginateBackward;
|
|
36
|
+
/** Walk `[from, to]` forward in ≤300-bucket windows (ranged/tail fetches). */
|
|
37
|
+
private paginateForward;
|
|
38
|
+
/** One candles request over `[startMs, endMs]` → ascending OHLCV (the API returns newest-first). */
|
|
39
|
+
private candlesChunk;
|
|
40
|
+
/**
|
|
41
|
+
* Build a forming candle from the Coinbase `ticker` channel (Coinbase has no native kline stream):
|
|
42
|
+
* the stream supplies a smooth live price/high/low, while a periodic REST re-seed fixes the
|
|
43
|
+
* authoritative open + volume and corrects any drift. A stall watchdog falls back to polling if the
|
|
44
|
+
* socket opens but never delivers a price; reconnects on an unexpected close until unsubscribed.
|
|
45
|
+
*/
|
|
46
|
+
private streamTicker;
|
|
47
|
+
/** Poll the forming candle (aggregated/W/M timeframes, or environments without WebSocket). */
|
|
48
|
+
private pollBars;
|
|
49
|
+
/** GET → parsed JSON, through the rate gate with 429 backoff. */
|
|
50
|
+
private json;
|
|
51
|
+
/**
|
|
52
|
+
* Issue one GET through the shared {@link gate} (concurrency + spacing), retrying after a 429
|
|
53
|
+
* (honoring `Retry-After`, else exponential backoff + jitter). Returns the `Response` so callers
|
|
54
|
+
* can read pagination headers (`cb-after`) before consuming the body.
|
|
55
|
+
*/
|
|
56
|
+
private request;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { CoinbaseProvider, DataProvider, ProviderInfo, SymbolDescriptor };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { O as OHLCV, U as Unsubscribe } from '../options-Q-576hIi.js';
|
|
2
|
+
import { D as DataProvider, P as ProviderInfo, B as BarRange, S as SymbolInfo, a as SymbolDescriptor } from '../DataProvider-Dzd-Erlk.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Coinbase market-data provider, built from scratch on the public Exchange REST + WebSocket APIs
|
|
6
|
+
* — no third-party SDK, no API key. Serves spot products (`BTC-USD`, `ETH-EUR`, …), paginates past the
|
|
7
|
+
* 300-candle cap, aggregates timeframes Coinbase doesn't serve natively (e.g. `30`, `4h`) and
|
|
8
|
+
* folds `W`/`M` from daily. Live ticks build a forming candle from the `ticker` stream (Coinbase
|
|
9
|
+
* has no native kline stream), with a poll fallback.
|
|
10
|
+
*
|
|
11
|
+
* Trades: Coinbase trades cursor-walk back from the live tip (the `cb-after` header) with no
|
|
12
|
+
* time-seek, so trade-derived depth is `'recent'` — live and recent windows reconstruct; older bars
|
|
13
|
+
* are served empty rather than walked unbounded.
|
|
14
|
+
*
|
|
15
|
+
* import { CoinbaseProvider } from 'vela/providers/coinbase';
|
|
16
|
+
* chart.data.registerProvider('coinbase', new CoinbaseProvider());
|
|
17
|
+
*/
|
|
18
|
+
declare class CoinbaseProvider implements DataProvider {
|
|
19
|
+
/** Cached symbol enumeration (the products list is large; fetch once). */
|
|
20
|
+
private symbolsPromise;
|
|
21
|
+
/** Shared request gate: caps concurrency, spaces request starts, honors 429 backoff. */
|
|
22
|
+
private readonly gate;
|
|
23
|
+
info(): ProviderInfo;
|
|
24
|
+
getBars(ticker: string, timeframe: string, range: BarRange): Promise<OHLCV[]>;
|
|
25
|
+
getSymbolInfo(ticker: string): Promise<SymbolInfo | undefined>;
|
|
26
|
+
listSymbols(): Promise<SymbolDescriptor[]>;
|
|
27
|
+
subscribe(ticker: string, timeframe: string, onBar: (bar: OHLCV) => void): Unsubscribe;
|
|
28
|
+
private fetchProducts;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch candles for a count (most-recent `limit`) or a `[from, to]` range, paginating past the
|
|
31
|
+
* 300-bucket cap. Returns ascending OHLCV (newest-first rows from the API are sorted on the way out).
|
|
32
|
+
*/
|
|
33
|
+
private fetchCandles;
|
|
34
|
+
/** Assemble the most-recent `limit` bars, walking backward in ≤300-bucket windows. */
|
|
35
|
+
private paginateBackward;
|
|
36
|
+
/** Walk `[from, to]` forward in ≤300-bucket windows (ranged/tail fetches). */
|
|
37
|
+
private paginateForward;
|
|
38
|
+
/** One candles request over `[startMs, endMs]` → ascending OHLCV (the API returns newest-first). */
|
|
39
|
+
private candlesChunk;
|
|
40
|
+
/**
|
|
41
|
+
* Build a forming candle from the Coinbase `ticker` channel (Coinbase has no native kline stream):
|
|
42
|
+
* the stream supplies a smooth live price/high/low, while a periodic REST re-seed fixes the
|
|
43
|
+
* authoritative open + volume and corrects any drift. A stall watchdog falls back to polling if the
|
|
44
|
+
* socket opens but never delivers a price; reconnects on an unexpected close until unsubscribed.
|
|
45
|
+
*/
|
|
46
|
+
private streamTicker;
|
|
47
|
+
/** Poll the forming candle (aggregated/W/M timeframes, or environments without WebSocket). */
|
|
48
|
+
private pollBars;
|
|
49
|
+
/** GET → parsed JSON, through the rate gate with 429 backoff. */
|
|
50
|
+
private json;
|
|
51
|
+
/**
|
|
52
|
+
* Issue one GET through the shared {@link gate} (concurrency + spacing), retrying after a 429
|
|
53
|
+
* (honoring `Retry-After`, else exponential backoff + jitter). Returns the `Response` so callers
|
|
54
|
+
* can read pagination headers (`cb-after`) before consuming the body.
|
|
55
|
+
*/
|
|
56
|
+
private request;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { CoinbaseProvider, DataProvider, ProviderInfo, SymbolDescriptor };
|