polaris-data 0.1.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/README.md +327 -0
- package/dist/index.cjs +751 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +323 -0
- package/dist/index.d.ts +323 -0
- package/dist/index.js +717 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DownloadNotAllowedError: () => DownloadNotAllowedError,
|
|
24
|
+
NotFoundError: () => NotFoundError,
|
|
25
|
+
OhlcvAggregator: () => OhlcvAggregator,
|
|
26
|
+
PolarisClient: () => PolarisClient,
|
|
27
|
+
PolarisError: () => PolarisError,
|
|
28
|
+
RateLimitedError: () => RateLimitedError,
|
|
29
|
+
StreamDecodeError: () => StreamDecodeError,
|
|
30
|
+
UnauthorizedError: () => UnauthorizedError
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/client.ts
|
|
35
|
+
var import_promises2 = require("fs/promises");
|
|
36
|
+
var import_node_path2 = require("path");
|
|
37
|
+
var import_fzstd = require("fzstd");
|
|
38
|
+
|
|
39
|
+
// src/errors.ts
|
|
40
|
+
var PolarisError = class extends Error {
|
|
41
|
+
name = "PolarisError";
|
|
42
|
+
/** HTTP status code, if the error originated from an API response. */
|
|
43
|
+
statusCode;
|
|
44
|
+
/** Raw response body string. */
|
|
45
|
+
body;
|
|
46
|
+
constructor(message, statusCode, body) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.statusCode = statusCode;
|
|
49
|
+
this.body = body;
|
|
50
|
+
}
|
|
51
|
+
toString() {
|
|
52
|
+
return this.statusCode != null ? `${this.message} (status=${this.statusCode})` : this.message;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var UnauthorizedError = class extends PolarisError {
|
|
56
|
+
name = "UnauthorizedError";
|
|
57
|
+
};
|
|
58
|
+
var NotFoundError = class extends PolarisError {
|
|
59
|
+
name = "NotFoundError";
|
|
60
|
+
};
|
|
61
|
+
var RateLimitedError = class extends PolarisError {
|
|
62
|
+
name = "RateLimitedError";
|
|
63
|
+
/** ISO 8601 timestamp (or epoch) indicating when the rate limit resets. */
|
|
64
|
+
resetAt;
|
|
65
|
+
constructor(message, statusCode, body, resetAt) {
|
|
66
|
+
super(message, statusCode, body);
|
|
67
|
+
this.resetAt = resetAt;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var StreamDecodeError = class extends PolarisError {
|
|
71
|
+
name = "StreamDecodeError";
|
|
72
|
+
};
|
|
73
|
+
var DownloadNotAllowedError = class extends PolarisError {
|
|
74
|
+
name = "DownloadNotAllowedError";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/utils.ts
|
|
78
|
+
var EPOCH_RE = /^\d+$/;
|
|
79
|
+
function toIso8601(value) {
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
if (EPOCH_RE.test(value) && value.length >= 13) return epochUsToIso(Number(value));
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (value instanceof Date) return dateToIso(value);
|
|
85
|
+
return value > 1e12 ? epochUsToIso(value) : epochMsToIso(value);
|
|
86
|
+
}
|
|
87
|
+
function toEpochUs(value) {
|
|
88
|
+
if (typeof value === "number") return value > 1e12 ? value : value * 1e3;
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
if (EPOCH_RE.test(value) && value.length >= 13) return Number(value);
|
|
91
|
+
return Math.round(new Date(value).getTime() * 1e3);
|
|
92
|
+
}
|
|
93
|
+
return Math.round(value.getTime() * 1e3);
|
|
94
|
+
}
|
|
95
|
+
function datesInRange(fromUs, toUs) {
|
|
96
|
+
const dates = [];
|
|
97
|
+
const start = new Date(fromUs / 1e3);
|
|
98
|
+
const end = new Date(toUs / 1e3);
|
|
99
|
+
const cur = new Date(
|
|
100
|
+
Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate())
|
|
101
|
+
);
|
|
102
|
+
const last = new Date(
|
|
103
|
+
Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate())
|
|
104
|
+
);
|
|
105
|
+
while (cur <= last) {
|
|
106
|
+
dates.push(formatUtcDate(cur));
|
|
107
|
+
cur.setUTCDate(cur.getUTCDate() + 1);
|
|
108
|
+
}
|
|
109
|
+
return dates;
|
|
110
|
+
}
|
|
111
|
+
function epochUsToIso(us) {
|
|
112
|
+
return epochMsToIso(us / 1e3);
|
|
113
|
+
}
|
|
114
|
+
function epochMsToIso(ms) {
|
|
115
|
+
return new Date(ms).toISOString().replace(/\.000Z$/, "Z");
|
|
116
|
+
}
|
|
117
|
+
function dateToIso(d) {
|
|
118
|
+
return d.toISOString().replace(/\.000Z$/, "Z");
|
|
119
|
+
}
|
|
120
|
+
function formatUtcDate(d) {
|
|
121
|
+
const y = d.getUTCFullYear();
|
|
122
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
123
|
+
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
124
|
+
return `${y}-${m}-${day}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/storage.ts
|
|
128
|
+
var import_promises = require("fs/promises");
|
|
129
|
+
var import_node_path = require("path");
|
|
130
|
+
var import_node_os = require("os");
|
|
131
|
+
function resolveRoot(explicit) {
|
|
132
|
+
if (explicit) return explicit;
|
|
133
|
+
if (process.env.POLARIS_ROOT) return process.env.POLARIS_ROOT;
|
|
134
|
+
if (process.env.POLARIS_DATASET_DOWNLOAD_DIR)
|
|
135
|
+
return process.env.POLARIS_DATASET_DOWNLOAD_DIR;
|
|
136
|
+
return defaultRoot();
|
|
137
|
+
}
|
|
138
|
+
function defaultRoot() {
|
|
139
|
+
switch ((0, import_node_os.platform)()) {
|
|
140
|
+
case "darwin":
|
|
141
|
+
return (0, import_node_path.join)(
|
|
142
|
+
(0, import_node_os.homedir)(),
|
|
143
|
+
"Library",
|
|
144
|
+
"Application Support",
|
|
145
|
+
"polaris"
|
|
146
|
+
);
|
|
147
|
+
case "win32":
|
|
148
|
+
return (0, import_node_path.join)(
|
|
149
|
+
process.env.APPDATA || (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Roaming"),
|
|
150
|
+
"polaris"
|
|
151
|
+
);
|
|
152
|
+
default:
|
|
153
|
+
return process.env.XDG_DATA_HOME ? (0, import_node_path.join)(process.env.XDG_DATA_HOME, "polaris") : (0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "share", "polaris");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function ensureLayout(root) {
|
|
157
|
+
const dataDir = (0, import_node_path.join)(root, "data");
|
|
158
|
+
const dailyDir = (0, import_node_path.join)(root, "daily");
|
|
159
|
+
const tmpDir = (0, import_node_path.join)(root, "tmp");
|
|
160
|
+
const cacheDir = (0, import_node_path.join)(root, "cache");
|
|
161
|
+
await Promise.all([
|
|
162
|
+
(0, import_promises.mkdir)(dataDir, { recursive: true }),
|
|
163
|
+
(0, import_promises.mkdir)(dailyDir, { recursive: true }),
|
|
164
|
+
(0, import_promises.mkdir)(tmpDir, { recursive: true }),
|
|
165
|
+
(0, import_promises.mkdir)(cacheDir, { recursive: true })
|
|
166
|
+
]);
|
|
167
|
+
return { root, dataDir, dailyDir, tmpDir, cacheDir };
|
|
168
|
+
}
|
|
169
|
+
function dataFilePath(dataDir, key) {
|
|
170
|
+
return (0, import_node_path.join)(dataDir, key);
|
|
171
|
+
}
|
|
172
|
+
function dailyFilePath(dailyDir, source, market, date) {
|
|
173
|
+
return (0, import_node_path.join)(dailyDir, source, market, `${date}.jsonl.zst`);
|
|
174
|
+
}
|
|
175
|
+
async function linkOrCopy(src, dest) {
|
|
176
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(dest), { recursive: true });
|
|
177
|
+
try {
|
|
178
|
+
await (0, import_promises.link)(src, dest);
|
|
179
|
+
} catch {
|
|
180
|
+
await (0, import_promises.copyFile)(src, dest);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function fileExists(path) {
|
|
184
|
+
try {
|
|
185
|
+
await (0, import_promises.stat)(path);
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/aggregator.ts
|
|
193
|
+
function intervalToUs(interval) {
|
|
194
|
+
const m = interval.match(/^(\d+)(ms|s|m|h)$/);
|
|
195
|
+
if (!m) throw new Error(`Invalid OHLCV interval: ${interval}`);
|
|
196
|
+
const n = parseInt(m[1], 10);
|
|
197
|
+
switch (m[2]) {
|
|
198
|
+
case "ms":
|
|
199
|
+
return n * 1e3;
|
|
200
|
+
case "s":
|
|
201
|
+
return n * 1e6;
|
|
202
|
+
case "m":
|
|
203
|
+
return n * 6e7;
|
|
204
|
+
case "h":
|
|
205
|
+
return n * 36e8;
|
|
206
|
+
default:
|
|
207
|
+
throw new Error(`Unreachable`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
var OhlcvAggregator = class {
|
|
211
|
+
_intervalUs;
|
|
212
|
+
_bars = /* @__PURE__ */ new Map();
|
|
213
|
+
constructor(interval) {
|
|
214
|
+
this._intervalUs = intervalToUs(interval);
|
|
215
|
+
}
|
|
216
|
+
/** Ingest a single trade event. */
|
|
217
|
+
add(timestamp, price, quantity) {
|
|
218
|
+
const bucketTs = Math.floor(timestamp / this._intervalUs) * this._intervalUs;
|
|
219
|
+
let b = this._bars.get(bucketTs);
|
|
220
|
+
if (!b) {
|
|
221
|
+
b = {
|
|
222
|
+
ts: bucketTs,
|
|
223
|
+
open: price,
|
|
224
|
+
high: price,
|
|
225
|
+
low: price,
|
|
226
|
+
close: price,
|
|
227
|
+
volScaled: 0,
|
|
228
|
+
trades: 0
|
|
229
|
+
};
|
|
230
|
+
this._bars.set(bucketTs, b);
|
|
231
|
+
}
|
|
232
|
+
if (price > b.high) b.high = price;
|
|
233
|
+
if (price < b.low) b.low = price;
|
|
234
|
+
b.close = price;
|
|
235
|
+
b.volScaled += Math.round(quantity * 1e12);
|
|
236
|
+
b.trades += 1;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Finalise the aggregation and return sorted bars.
|
|
240
|
+
*
|
|
241
|
+
* The `open` of each subsequent bar is overwritten with the `close` of the
|
|
242
|
+
* previous bar, matching the convention used by the Python SDK.
|
|
243
|
+
*/
|
|
244
|
+
finish() {
|
|
245
|
+
const bars = Array.from(this._bars.values()).sort(
|
|
246
|
+
(a, b) => a.ts - b.ts
|
|
247
|
+
);
|
|
248
|
+
for (let i = 1; i < bars.length; i++) {
|
|
249
|
+
bars[i].open = bars[i - 1].close;
|
|
250
|
+
}
|
|
251
|
+
return bars.map((b) => ({
|
|
252
|
+
timestamp: b.ts,
|
|
253
|
+
open: b.open,
|
|
254
|
+
high: b.high,
|
|
255
|
+
low: b.low,
|
|
256
|
+
close: b.close,
|
|
257
|
+
volume: b.volScaled / 1e12,
|
|
258
|
+
trades: b.trades
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/client.ts
|
|
264
|
+
var VERSION = "0.1.0";
|
|
265
|
+
var PolarisClient = class {
|
|
266
|
+
_apiKey;
|
|
267
|
+
_baseUrl;
|
|
268
|
+
_timeout;
|
|
269
|
+
_fetch;
|
|
270
|
+
_root;
|
|
271
|
+
_layout;
|
|
272
|
+
constructor(options = {}) {
|
|
273
|
+
this._apiKey = options.apiKey ?? readEnvApiKey();
|
|
274
|
+
this._baseUrl = new URL(options.baseUrl ?? "https://api.polaris.supply");
|
|
275
|
+
this._timeout = options.timeout ?? 3e4;
|
|
276
|
+
this._fetch = options.fetch ?? globalThis.fetch;
|
|
277
|
+
this._root = resolveRoot(options.datasetRoot);
|
|
278
|
+
}
|
|
279
|
+
// -----------------------------------------------------------------------
|
|
280
|
+
// Discovery
|
|
281
|
+
// -----------------------------------------------------------------------
|
|
282
|
+
/** Check API availability. */
|
|
283
|
+
async health() {
|
|
284
|
+
return this._getJson("/health", { auth: "none" });
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Browse supported sources and markets.
|
|
288
|
+
*
|
|
289
|
+
* If neither `source` nor `market` is provided, returns the full catalog.
|
|
290
|
+
* `market` requires `source`.
|
|
291
|
+
*/
|
|
292
|
+
async catalog(options = {}) {
|
|
293
|
+
const params = {};
|
|
294
|
+
if (options.source) params.source = options.source;
|
|
295
|
+
if (options.market) params.market = options.market;
|
|
296
|
+
return this._getJson("/catalog", { params, auth: "if-available" });
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* List available snapshot files for a source and market over a time range.
|
|
300
|
+
* Auto-paginates to return **all** matching entries.
|
|
301
|
+
*/
|
|
302
|
+
async listSnapshots(options) {
|
|
303
|
+
const entries = [];
|
|
304
|
+
let cursor;
|
|
305
|
+
do {
|
|
306
|
+
const params = buildSnapshotParams(options);
|
|
307
|
+
if (cursor) params.cursor = cursor;
|
|
308
|
+
const res = await this._getJson("/snapshots", { params, auth: "if-available" });
|
|
309
|
+
entries.push(...res.snapshots);
|
|
310
|
+
cursor = res.has_more ? res.next_cursor ?? void 0 : void 0;
|
|
311
|
+
} while (cursor);
|
|
312
|
+
return entries;
|
|
313
|
+
}
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
// Historical data – snapshot-first
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
/**
|
|
318
|
+
* Return all standardised historical events for a time range.
|
|
319
|
+
*
|
|
320
|
+
* Reads from locally-cached daily `.jsonl.zst` snapshot files.
|
|
321
|
+
* Missing daily artifacts are discovered via `GET /snapshots` and
|
|
322
|
+
* downloaded automatically.
|
|
323
|
+
*/
|
|
324
|
+
async events(options) {
|
|
325
|
+
const fromUs = toEpochUs(options.from);
|
|
326
|
+
const toUs = toEpochUs(options.to);
|
|
327
|
+
const result = [];
|
|
328
|
+
for await (const event of this._readDailyEvents(
|
|
329
|
+
options.source,
|
|
330
|
+
options.market,
|
|
331
|
+
fromUs,
|
|
332
|
+
toUs
|
|
333
|
+
)) {
|
|
334
|
+
result.push(event);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Return all standardised trade events for a time range.
|
|
340
|
+
*
|
|
341
|
+
* Reads from locally-cached daily snapshot files, filtering to
|
|
342
|
+
* `type === "trade"`.
|
|
343
|
+
*/
|
|
344
|
+
async trades(options) {
|
|
345
|
+
const fromUs = toEpochUs(options.from);
|
|
346
|
+
const toUs = toEpochUs(options.to);
|
|
347
|
+
const result = [];
|
|
348
|
+
for await (const event of this._readDailyEvents(
|
|
349
|
+
options.source,
|
|
350
|
+
options.market,
|
|
351
|
+
fromUs,
|
|
352
|
+
toUs,
|
|
353
|
+
(e) => e.type === "trade"
|
|
354
|
+
)) {
|
|
355
|
+
result.push(event);
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Aggregate OHLCV bars from standardised trade data.
|
|
361
|
+
*
|
|
362
|
+
* Reads from locally-cached daily snapshot files and aggregates in memory
|
|
363
|
+
* using the same interval-bucketing strategy as the Python SDK.
|
|
364
|
+
*/
|
|
365
|
+
async ohlcv(options) {
|
|
366
|
+
const fromUs = toEpochUs(options.from);
|
|
367
|
+
const toUs = toEpochUs(options.to);
|
|
368
|
+
const agg = new OhlcvAggregator(options.interval);
|
|
369
|
+
for await (const event of this._readDailyEvents(
|
|
370
|
+
options.source,
|
|
371
|
+
options.market,
|
|
372
|
+
fromUs,
|
|
373
|
+
toUs,
|
|
374
|
+
(e) => e.type === "trade"
|
|
375
|
+
)) {
|
|
376
|
+
const data = event.data;
|
|
377
|
+
agg.add(event.timestamp, data.price, data.quantity);
|
|
378
|
+
}
|
|
379
|
+
return agg.finish();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Return a TradingView-shaped OHLCV response.
|
|
383
|
+
*
|
|
384
|
+
* Aggregates bars from local snapshot data and reshapes to
|
|
385
|
+
* `{ candles, volumes }`.
|
|
386
|
+
*/
|
|
387
|
+
async ohlcvTradingView(options) {
|
|
388
|
+
const bars = await this.ohlcv(options);
|
|
389
|
+
return {
|
|
390
|
+
candles: bars.map((b) => ({
|
|
391
|
+
t: b.timestamp,
|
|
392
|
+
o: b.open,
|
|
393
|
+
h: b.high,
|
|
394
|
+
l: b.low,
|
|
395
|
+
c: b.close
|
|
396
|
+
})),
|
|
397
|
+
volumes: bars.map((b) => ({
|
|
398
|
+
t: b.timestamp,
|
|
399
|
+
v: b.volume,
|
|
400
|
+
trades: b.trades
|
|
401
|
+
}))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Stream historical events as an async iterable.
|
|
406
|
+
*
|
|
407
|
+
* Defaults to standardised events from local snapshots (`standard: true`).
|
|
408
|
+
* Pass `standard: false` to stream raw payloads via the `/raw` endpoint.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```ts
|
|
412
|
+
* for await (const row of client.replay({
|
|
413
|
+
* source: "binance",
|
|
414
|
+
* market: "BTC-USDT",
|
|
415
|
+
* from: "2024-01-01T00:00:00Z",
|
|
416
|
+
* to: "2024-01-01T01:00:00Z",
|
|
417
|
+
* })) {
|
|
418
|
+
* console.log(row);
|
|
419
|
+
* }
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
async *replay(options) {
|
|
423
|
+
if (options.standard !== false) {
|
|
424
|
+
const fromUs = toEpochUs(options.from);
|
|
425
|
+
const toUs = toEpochUs(options.to);
|
|
426
|
+
yield* this._readDailyEvents(
|
|
427
|
+
options.source,
|
|
428
|
+
options.market,
|
|
429
|
+
fromUs,
|
|
430
|
+
toUs
|
|
431
|
+
);
|
|
432
|
+
} else {
|
|
433
|
+
yield* this._streamRaw(options);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// -----------------------------------------------------------------------
|
|
437
|
+
// Raw (API-only, not snapshot-first)
|
|
438
|
+
// -----------------------------------------------------------------------
|
|
439
|
+
/**
|
|
440
|
+
* Return raw venue-native payloads for a time range.
|
|
441
|
+
* Requires an API key. Uses the `/raw` endpoint with pagination.
|
|
442
|
+
*/
|
|
443
|
+
async raw(options) {
|
|
444
|
+
const params = buildRawParams(options);
|
|
445
|
+
return this._paginateAll("/raw", params, "required");
|
|
446
|
+
}
|
|
447
|
+
// -----------------------------------------------------------------------
|
|
448
|
+
// Downloads
|
|
449
|
+
// -----------------------------------------------------------------------
|
|
450
|
+
/**
|
|
451
|
+
* Download a single snapshot file by key.
|
|
452
|
+
*
|
|
453
|
+
* Returns the native `Response` so you can consume the body as needed
|
|
454
|
+
* (`.arrayBuffer()`, `.blob()`, or pipe to a writable stream).
|
|
455
|
+
*/
|
|
456
|
+
async downloadSnapshot(options) {
|
|
457
|
+
const params = { key: options.key };
|
|
458
|
+
if (options.filename) params.filename = options.filename;
|
|
459
|
+
if (options.mode) params.mode = options.mode;
|
|
460
|
+
const { response, body } = await this._fetchRaw("/snapshots/download", {
|
|
461
|
+
params,
|
|
462
|
+
auth: "if-available"
|
|
463
|
+
});
|
|
464
|
+
assertOk(response, body);
|
|
465
|
+
return response;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get a pre-signed download URL for a snapshot file
|
|
469
|
+
* without fetching the file itself.
|
|
470
|
+
*/
|
|
471
|
+
async getSnapshotDownloadUrl(options) {
|
|
472
|
+
const params = { key: options.key, mode: "url" };
|
|
473
|
+
if (options.filename) params.filename = options.filename;
|
|
474
|
+
return this._getJson("/snapshots/download", {
|
|
475
|
+
params,
|
|
476
|
+
auth: "if-available"
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// -----------------------------------------------------------------------
|
|
480
|
+
// Lifecycle
|
|
481
|
+
// -----------------------------------------------------------------------
|
|
482
|
+
/** Release resources. Currently a no-op (reserved for future use). */
|
|
483
|
+
close() {
|
|
484
|
+
}
|
|
485
|
+
/** Async disposable support (Node ≥ 18 / TypeScript ≥ 5.2). */
|
|
486
|
+
async [Symbol.asyncDispose]() {
|
|
487
|
+
this.close();
|
|
488
|
+
}
|
|
489
|
+
// -----------------------------------------------------------------------
|
|
490
|
+
// Internals – snapshot-first local reads
|
|
491
|
+
// -----------------------------------------------------------------------
|
|
492
|
+
/**
|
|
493
|
+
* Core routine: ensure daily `.jsonl.zst` artifacts exist, decompress them,
|
|
494
|
+
* and yield matching events one at a time.
|
|
495
|
+
*/
|
|
496
|
+
async *_readDailyEvents(source, market, fromUs, toUs, filter) {
|
|
497
|
+
const layout = await this._getLayout();
|
|
498
|
+
const dates = datesInRange(fromUs, toUs);
|
|
499
|
+
const filePaths = await this._ensureDailyArtifacts(
|
|
500
|
+
source,
|
|
501
|
+
market,
|
|
502
|
+
dates,
|
|
503
|
+
layout
|
|
504
|
+
);
|
|
505
|
+
for (const filePath of filePaths) {
|
|
506
|
+
const lines = await readDailyLines(filePath);
|
|
507
|
+
for (const line of lines) {
|
|
508
|
+
let event;
|
|
509
|
+
try {
|
|
510
|
+
event = JSON.parse(line);
|
|
511
|
+
} catch {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const ts = event.timestamp;
|
|
515
|
+
if (ts < fromUs || ts >= toUs) continue;
|
|
516
|
+
if (filter && !filter(event)) continue;
|
|
517
|
+
yield event;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Ensure every requested date has a materialised daily artifact.
|
|
523
|
+
* Downloads missing snapshots and materialises them into `daily/`.
|
|
524
|
+
*/
|
|
525
|
+
async _ensureDailyArtifacts(source, market, dates, layout) {
|
|
526
|
+
const paths = [];
|
|
527
|
+
for (const date of dates) {
|
|
528
|
+
const dailyPath = dailyFilePath(layout.dailyDir, source, market, date);
|
|
529
|
+
if (await fileExists(dailyPath)) {
|
|
530
|
+
paths.push(dailyPath);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const snapshots = await this.listSnapshots({
|
|
534
|
+
source,
|
|
535
|
+
market,
|
|
536
|
+
from: `${date}T00:00:00Z`,
|
|
537
|
+
to: `${date}T23:59:59Z`
|
|
538
|
+
});
|
|
539
|
+
const entry = snapshots.find((s) => s.date === date);
|
|
540
|
+
if (!entry) {
|
|
541
|
+
throw new PolarisError(
|
|
542
|
+
`No snapshot available for ${source}/${market} on ${date}`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
await this._downloadAndMaterialise(entry.key, dailyPath, layout);
|
|
546
|
+
paths.push(dailyPath);
|
|
547
|
+
}
|
|
548
|
+
return paths;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Download a snapshot to `data/` and create a hardlink (or copy) into
|
|
552
|
+
* `daily/`.
|
|
553
|
+
*/
|
|
554
|
+
async _downloadAndMaterialise(key, dailyPath, layout) {
|
|
555
|
+
const dataPath = dataFilePath(layout.dataDir, key);
|
|
556
|
+
if (!await fileExists(dataPath)) {
|
|
557
|
+
const urlInfo = await this.getSnapshotDownloadUrl({ key });
|
|
558
|
+
const response = await this._fetch(urlInfo.url);
|
|
559
|
+
if (!response.ok) {
|
|
560
|
+
throw new PolarisError(
|
|
561
|
+
`Failed to download snapshot ${key}: HTTP ${response.status}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
565
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(dataPath), { recursive: true });
|
|
566
|
+
await (0, import_promises2.writeFile)(dataPath, buffer);
|
|
567
|
+
}
|
|
568
|
+
await linkOrCopy(dataPath, dailyPath);
|
|
569
|
+
}
|
|
570
|
+
// -----------------------------------------------------------------------
|
|
571
|
+
// Internals – raw endpoint streaming
|
|
572
|
+
// -----------------------------------------------------------------------
|
|
573
|
+
async *_streamRaw(options) {
|
|
574
|
+
const params = {
|
|
575
|
+
source: options.source,
|
|
576
|
+
market: options.market,
|
|
577
|
+
from: toIso8601(options.from),
|
|
578
|
+
to: toIso8601(options.to)
|
|
579
|
+
};
|
|
580
|
+
let cursor;
|
|
581
|
+
do {
|
|
582
|
+
if (cursor) params.cursor = cursor;
|
|
583
|
+
const res = await this._getJson("/raw", {
|
|
584
|
+
params,
|
|
585
|
+
auth: "required"
|
|
586
|
+
});
|
|
587
|
+
for (const item of res.data) {
|
|
588
|
+
yield item;
|
|
589
|
+
}
|
|
590
|
+
cursor = res.next_cursor ?? void 0;
|
|
591
|
+
} while (cursor);
|
|
592
|
+
}
|
|
593
|
+
// -----------------------------------------------------------------------
|
|
594
|
+
// Internals – HTTP layer
|
|
595
|
+
// -----------------------------------------------------------------------
|
|
596
|
+
async _getJson(path, opts = {}) {
|
|
597
|
+
const { response, body } = await this._fetchRaw(path, {
|
|
598
|
+
...opts,
|
|
599
|
+
headers: { Accept: "application/json", ...opts.headers }
|
|
600
|
+
});
|
|
601
|
+
assertOk(response, body);
|
|
602
|
+
let json;
|
|
603
|
+
try {
|
|
604
|
+
json = JSON.parse(body);
|
|
605
|
+
} catch {
|
|
606
|
+
throw new PolarisError("Failed to parse response as JSON");
|
|
607
|
+
}
|
|
608
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
609
|
+
throw new PolarisError("Expected a JSON object response");
|
|
610
|
+
}
|
|
611
|
+
return json;
|
|
612
|
+
}
|
|
613
|
+
async _fetchRaw(path, opts = {}) {
|
|
614
|
+
const headers = this._buildHeaders(opts.auth ?? "none", opts.headers);
|
|
615
|
+
const url = this._buildUrl(path, opts.params);
|
|
616
|
+
const controller = new AbortController();
|
|
617
|
+
const timer = setTimeout(() => controller.abort(), this._timeout);
|
|
618
|
+
let response;
|
|
619
|
+
try {
|
|
620
|
+
response = await this._fetch(url, {
|
|
621
|
+
headers,
|
|
622
|
+
signal: controller.signal,
|
|
623
|
+
redirect: "follow"
|
|
624
|
+
});
|
|
625
|
+
} catch (e) {
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
if (e instanceof Error && e.name === "AbortError") {
|
|
628
|
+
throw new PolarisError("Request timed out");
|
|
629
|
+
}
|
|
630
|
+
throw new PolarisError(`Request failed: ${e}`);
|
|
631
|
+
}
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
const body = await response.text();
|
|
634
|
+
return { response, body };
|
|
635
|
+
}
|
|
636
|
+
_buildHeaders(authMode, extra) {
|
|
637
|
+
const out = {
|
|
638
|
+
"User-Agent": `polaris-ts/${VERSION}`,
|
|
639
|
+
...extra
|
|
640
|
+
};
|
|
641
|
+
if (authMode === "required" && !this._apiKey) {
|
|
642
|
+
throw new UnauthorizedError("API key is required for this endpoint");
|
|
643
|
+
}
|
|
644
|
+
if (this._apiKey && authMode !== "none") {
|
|
645
|
+
out["Authorization"] = `Bearer ${this._apiKey}`;
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
_buildUrl(path, params) {
|
|
650
|
+
const url = new URL(path, this._baseUrl);
|
|
651
|
+
if (params) {
|
|
652
|
+
for (const [k, v] of Object.entries(params)) {
|
|
653
|
+
if (v !== void 0) url.searchParams.set(k, v);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return url.toString();
|
|
657
|
+
}
|
|
658
|
+
// -----------------------------------------------------------------------
|
|
659
|
+
// Internals – pagination
|
|
660
|
+
// -----------------------------------------------------------------------
|
|
661
|
+
async _paginateAll(path, baseParams, auth) {
|
|
662
|
+
const items = [];
|
|
663
|
+
let cursor;
|
|
664
|
+
do {
|
|
665
|
+
const params = cursor ? { ...baseParams, cursor } : { ...baseParams };
|
|
666
|
+
const res = await this._getJson(path, {
|
|
667
|
+
params,
|
|
668
|
+
auth
|
|
669
|
+
});
|
|
670
|
+
items.push(...res.data);
|
|
671
|
+
cursor = res.next_cursor ?? void 0;
|
|
672
|
+
} while (cursor);
|
|
673
|
+
return items;
|
|
674
|
+
}
|
|
675
|
+
// -----------------------------------------------------------------------
|
|
676
|
+
// Internals – layout lazy init
|
|
677
|
+
// -----------------------------------------------------------------------
|
|
678
|
+
async _getLayout() {
|
|
679
|
+
if (!this._layout) this._layout = await ensureLayout(this._root);
|
|
680
|
+
return this._layout;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
function readEnvApiKey() {
|
|
684
|
+
try {
|
|
685
|
+
return process?.env?.POLARIS_API_KEY;
|
|
686
|
+
} catch {
|
|
687
|
+
return void 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function buildRawParams(options) {
|
|
691
|
+
const p = {
|
|
692
|
+
source: options.source,
|
|
693
|
+
market: options.market
|
|
694
|
+
};
|
|
695
|
+
if (options.from !== void 0) p.from = toIso8601(options.from);
|
|
696
|
+
if (options.to !== void 0) p.to = toIso8601(options.to);
|
|
697
|
+
if (options.limit !== void 0) p.limit = String(options.limit);
|
|
698
|
+
if (options.format) p.format = options.format;
|
|
699
|
+
return p;
|
|
700
|
+
}
|
|
701
|
+
function buildSnapshotParams(options) {
|
|
702
|
+
const p = {
|
|
703
|
+
source: options.source,
|
|
704
|
+
market: options.market
|
|
705
|
+
};
|
|
706
|
+
if (options.from !== void 0) p.from = toIso8601(options.from);
|
|
707
|
+
if (options.to !== void 0) p.to = toIso8601(options.to);
|
|
708
|
+
if (options.limit !== void 0) p.limit = String(options.limit);
|
|
709
|
+
return p;
|
|
710
|
+
}
|
|
711
|
+
function assertOk(response, body) {
|
|
712
|
+
if (response.ok) return;
|
|
713
|
+
let message = `HTTP ${response.status}`;
|
|
714
|
+
let resetAt;
|
|
715
|
+
try {
|
|
716
|
+
const json = JSON.parse(body);
|
|
717
|
+
if (typeof json === "object" && json !== null) {
|
|
718
|
+
message = String(json.error ?? json.message ?? message);
|
|
719
|
+
resetAt = json.reset_at;
|
|
720
|
+
}
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
switch (response.status) {
|
|
724
|
+
case 401:
|
|
725
|
+
throw new UnauthorizedError(message, response.status, body);
|
|
726
|
+
case 404:
|
|
727
|
+
throw new NotFoundError(message, response.status, body);
|
|
728
|
+
case 429:
|
|
729
|
+
throw new RateLimitedError(message, response.status, body, resetAt);
|
|
730
|
+
default:
|
|
731
|
+
throw new PolarisError(message, response.status, body);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function readDailyLines(filePath) {
|
|
735
|
+
const compressed = await (0, import_promises2.readFile)(filePath);
|
|
736
|
+
const decompressed = (0, import_fzstd.decompress)(compressed);
|
|
737
|
+
const text = new TextDecoder().decode(decompressed);
|
|
738
|
+
return text.split("\n").filter((l) => l.trim().length > 0);
|
|
739
|
+
}
|
|
740
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
741
|
+
0 && (module.exports = {
|
|
742
|
+
DownloadNotAllowedError,
|
|
743
|
+
NotFoundError,
|
|
744
|
+
OhlcvAggregator,
|
|
745
|
+
PolarisClient,
|
|
746
|
+
PolarisError,
|
|
747
|
+
RateLimitedError,
|
|
748
|
+
StreamDecodeError,
|
|
749
|
+
UnauthorizedError
|
|
750
|
+
});
|
|
751
|
+
//# sourceMappingURL=index.cjs.map
|