@tradejs/infra 1.0.4 → 1.0.6

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/dist/ai.mjs ADDED
@@ -0,0 +1,386 @@
1
+ import {
2
+ mergeJsonlFiles,
3
+ toFileToken
4
+ } from "./chunk-KVEZORMS.mjs";
5
+
6
+ // src/aiDatasetFile.ts
7
+ import { once } from "events";
8
+ import { createWriteStream } from "fs";
9
+ import fs from "fs/promises";
10
+ import path from "path";
11
+ import { createReadStream } from "fs";
12
+ import readline from "readline";
13
+ var DEFAULT_DIR = "data/ai/export";
14
+ var AI_DATASET_WRITE_BATCH_SIZE = 100;
15
+ var AI_MERGE_SORT_RUN_MAX_ROWS = 2e3;
16
+ var AI_MERGE_SORT_RUN_MAX_BYTES = 16 * 1024 * 1024;
17
+ var AI_CHUNK_FILE_RE = /^ai-dataset-(.+)-chunk-[^.]+\.jsonl$/;
18
+ var writerByPath = /* @__PURE__ */ new Map();
19
+ var getAiChunkFilePath = (strategyName, chunkId, outDir = DEFAULT_DIR) => path.join(
20
+ outDir,
21
+ `ai-dataset-${toFileToken(strategyName)}-chunk-${toFileToken(chunkId)}.jsonl`
22
+ );
23
+ var appendAiDatasetRow = async (params) => {
24
+ const { strategyName, chunkId, row, outDir = DEFAULT_DIR } = params;
25
+ const filePath = getAiChunkFilePath(strategyName, chunkId, outDir);
26
+ let state = writerByPath.get(filePath);
27
+ if (!state) {
28
+ await fs.mkdir(outDir, { recursive: true });
29
+ const stream = createWriteStream(filePath, {
30
+ encoding: "utf8",
31
+ flags: "a"
32
+ });
33
+ state = {
34
+ filePath,
35
+ stream,
36
+ buffer: [],
37
+ writeQueue: Promise.resolve(),
38
+ closed: false
39
+ };
40
+ writerByPath.set(filePath, state);
41
+ }
42
+ if (state.closed) {
43
+ throw new Error(`AI dataset writer is closed: ${filePath}`);
44
+ }
45
+ state.buffer.push(`${JSON.stringify(row)}
46
+ `);
47
+ if (state.buffer.length >= AI_DATASET_WRITE_BATCH_SIZE) {
48
+ await flushAiDatasetWriter(filePath);
49
+ }
50
+ return filePath;
51
+ };
52
+ var flushState = async (state) => {
53
+ if (state.closed || state.buffer.length === 0) {
54
+ return;
55
+ }
56
+ const chunk = state.buffer.join("");
57
+ state.buffer = [];
58
+ if (!state.stream.write(chunk)) {
59
+ await once(state.stream, "drain");
60
+ }
61
+ };
62
+ var flushAiDatasetWriter = async (filePath) => {
63
+ const state = writerByPath.get(filePath);
64
+ if (!state || state.closed) {
65
+ return;
66
+ }
67
+ state.writeQueue = state.writeQueue.then(() => flushState(state));
68
+ await state.writeQueue;
69
+ };
70
+ var closeState = async (state) => {
71
+ if (state.closed) {
72
+ return;
73
+ }
74
+ await flushState(state);
75
+ state.closed = true;
76
+ state.stream.end();
77
+ await Promise.all([
78
+ once(state.stream, "finish"),
79
+ once(state.stream, "close")
80
+ ]);
81
+ };
82
+ var closeAiDatasetWriter = async (filePath) => {
83
+ const state = writerByPath.get(filePath);
84
+ if (!state) {
85
+ return;
86
+ }
87
+ state.writeQueue = state.writeQueue.then(() => closeState(state));
88
+ await state.writeQueue;
89
+ writerByPath.delete(filePath);
90
+ };
91
+ var closeAllAiDatasetWriters = async () => {
92
+ const filePaths = [...writerByPath.keys()];
93
+ for (const filePath of filePaths) {
94
+ await closeAiDatasetWriter(filePath);
95
+ }
96
+ };
97
+ var listAiChunkFiles = async (params) => {
98
+ const { strategyName, outDir = DEFAULT_DIR } = params;
99
+ const prefix = `ai-dataset-${toFileToken(strategyName)}-chunk-`;
100
+ let entries = [];
101
+ try {
102
+ entries = await fs.readdir(outDir);
103
+ } catch {
104
+ return [];
105
+ }
106
+ return entries.filter((name) => name.startsWith(prefix) && name.endsWith(".jsonl")).map((name) => path.join(outDir, name)).sort();
107
+ };
108
+ var listAiChunkStrategies = async (params) => {
109
+ const outDir = params?.outDir ?? DEFAULT_DIR;
110
+ let entries = [];
111
+ try {
112
+ entries = await fs.readdir(outDir);
113
+ } catch {
114
+ return [];
115
+ }
116
+ return [
117
+ ...new Set(
118
+ entries.map((name) => name.match(AI_CHUNK_FILE_RE)?.[1] || "").filter(Boolean)
119
+ )
120
+ ].sort();
121
+ };
122
+ var parseAiDatasetLine = (line, filePath) => {
123
+ try {
124
+ return JSON.parse(line);
125
+ } catch (error) {
126
+ const message = error?.message || String(error);
127
+ throw new Error(
128
+ `Failed to parse AI dataset row from ${filePath}: ${message}`
129
+ );
130
+ }
131
+ };
132
+ var getAiDatasetSortKey = (row) => {
133
+ const timestamp = Number(row.timestamp);
134
+ return {
135
+ timestamp: Number.isFinite(timestamp) ? timestamp : Number.MAX_SAFE_INTEGER,
136
+ symbol: String(row.symbol || ""),
137
+ signalId: String(row.signalId || "")
138
+ };
139
+ };
140
+ var compareAiDatasetSortKeys = (left, right) => {
141
+ if (left.timestamp !== right.timestamp) {
142
+ return left.timestamp - right.timestamp;
143
+ }
144
+ const symbolCompare = left.symbol.localeCompare(right.symbol);
145
+ if (symbolCompare !== 0) {
146
+ return symbolCompare;
147
+ }
148
+ return left.signalId.localeCompare(right.signalId);
149
+ };
150
+ var compareSortableAiDatasetLines = (left, right) => {
151
+ const keyCompare = compareAiDatasetSortKeys(left.sortKey, right.sortKey);
152
+ if (keyCompare !== 0) {
153
+ return keyCompare;
154
+ }
155
+ return left.sourceIndex - right.sourceIndex;
156
+ };
157
+ var compareSortedRunHeads = (left, right) => {
158
+ const keyCompare = compareAiDatasetSortKeys(left.sortKey, right.sortKey);
159
+ if (keyCompare !== 0) {
160
+ return keyCompare;
161
+ }
162
+ return left.runIndex - right.runIndex;
163
+ };
164
+ var writeJsonlLines = async (params) => {
165
+ const { filePath, lines } = params;
166
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
167
+ const stream = createWriteStream(filePath, { encoding: "utf8" });
168
+ const done = Promise.all([once(stream, "finish"), once(stream, "close")]);
169
+ try {
170
+ for (const line of lines) {
171
+ if (!stream.write(`${line}
172
+ `)) {
173
+ await once(stream, "drain");
174
+ }
175
+ }
176
+ } finally {
177
+ stream.end();
178
+ await done;
179
+ }
180
+ };
181
+ var flushSortedRun = async (params) => {
182
+ const { tempDir, runIndex, entries } = params;
183
+ entries.sort(compareSortableAiDatasetLines);
184
+ const filePath = path.join(
185
+ tempDir,
186
+ `run-${String(runIndex).padStart(6, "0")}.jsonl`
187
+ );
188
+ await writeJsonlLines({
189
+ filePath,
190
+ lines: entries.map(({ line }) => line)
191
+ });
192
+ return filePath;
193
+ };
194
+ var readNextNonEmptyLine = async (iterator) => {
195
+ while (true) {
196
+ const next = await iterator.next();
197
+ if (next.done) {
198
+ return null;
199
+ }
200
+ const trimmed = String(next.value || "").trim();
201
+ if (trimmed) {
202
+ return trimmed;
203
+ }
204
+ }
205
+ };
206
+ var mergeSortedRuns = async (params) => {
207
+ const { runPaths, outPath } = params;
208
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
209
+ const output = createWriteStream(outPath, { encoding: "utf8" });
210
+ const outputDone = Promise.all([
211
+ once(output, "finish"),
212
+ once(output, "close")
213
+ ]);
214
+ const heads = [];
215
+ try {
216
+ for (let runIndex = 0; runIndex < runPaths.length; runIndex += 1) {
217
+ const runPath = runPaths[runIndex];
218
+ const reader = readline.createInterface({
219
+ input: createReadStream(runPath, { encoding: "utf8" }),
220
+ crlfDelay: Infinity
221
+ });
222
+ const iterator = reader[Symbol.asyncIterator]();
223
+ const line = await readNextNonEmptyLine(iterator);
224
+ if (!line) {
225
+ reader.close();
226
+ continue;
227
+ }
228
+ heads.push({
229
+ runIndex,
230
+ line,
231
+ sortKey: getAiDatasetSortKey(parseAiDatasetLine(line, runPath)),
232
+ iterator,
233
+ reader
234
+ });
235
+ }
236
+ while (heads.length > 0) {
237
+ let bestIndex = 0;
238
+ for (let index = 1; index < heads.length; index += 1) {
239
+ if (compareSortedRunHeads(heads[index], heads[bestIndex]) < 0) {
240
+ bestIndex = index;
241
+ }
242
+ }
243
+ const best = heads[bestIndex];
244
+ if (!output.write(`${best.line}
245
+ `)) {
246
+ await once(output, "drain");
247
+ }
248
+ const nextLine = await readNextNonEmptyLine(best.iterator);
249
+ if (!nextLine) {
250
+ best.reader.close();
251
+ heads.splice(bestIndex, 1);
252
+ continue;
253
+ }
254
+ best.line = nextLine;
255
+ best.sortKey = getAiDatasetSortKey(
256
+ parseAiDatasetLine(nextLine, runPaths[best.runIndex])
257
+ );
258
+ }
259
+ } finally {
260
+ for (const head of heads) {
261
+ head.reader.close();
262
+ }
263
+ output.end();
264
+ await outputDone;
265
+ }
266
+ };
267
+ var mergeAiJsonlFiles = async (params) => {
268
+ const {
269
+ filePaths,
270
+ outPath,
271
+ maxRowsInMemory = AI_MERGE_SORT_RUN_MAX_ROWS,
272
+ maxBytesInMemory = AI_MERGE_SORT_RUN_MAX_BYTES
273
+ } = params;
274
+ const tempDir = path.join(
275
+ path.dirname(outPath),
276
+ `.ai-merge-${path.basename(outPath)}-${Date.now()}-${process.pid}`
277
+ );
278
+ let batch = [];
279
+ let batchBytes = 0;
280
+ let sourceIndex = 0;
281
+ let runIndex = 0;
282
+ const runPaths = [];
283
+ const flushBatch = async () => {
284
+ if (!batch.length) {
285
+ return;
286
+ }
287
+ runPaths.push(
288
+ await flushSortedRun({
289
+ tempDir,
290
+ runIndex,
291
+ entries: batch
292
+ })
293
+ );
294
+ runIndex += 1;
295
+ batch = [];
296
+ batchBytes = 0;
297
+ };
298
+ try {
299
+ await fs.mkdir(tempDir, { recursive: true });
300
+ for (const filePath of filePaths) {
301
+ const reader = readline.createInterface({
302
+ input: createReadStream(filePath, { encoding: "utf8" }),
303
+ crlfDelay: Infinity
304
+ });
305
+ try {
306
+ for await (const line of reader) {
307
+ const trimmed = line.trim();
308
+ if (!trimmed) {
309
+ continue;
310
+ }
311
+ batch.push({
312
+ line: trimmed,
313
+ sortKey: getAiDatasetSortKey(parseAiDatasetLine(trimmed, filePath)),
314
+ sourceIndex
315
+ });
316
+ sourceIndex += 1;
317
+ batchBytes += Buffer.byteLength(trimmed, "utf8") + 1;
318
+ if (batch.length >= maxRowsInMemory || batchBytes >= maxBytesInMemory) {
319
+ await flushBatch();
320
+ }
321
+ }
322
+ } finally {
323
+ reader.close();
324
+ }
325
+ }
326
+ await flushBatch();
327
+ if (!runPaths.length) {
328
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
329
+ await fs.writeFile(outPath, "", "utf8");
330
+ return;
331
+ }
332
+ await mergeSortedRuns({
333
+ runPaths,
334
+ outPath
335
+ });
336
+ } finally {
337
+ await fs.rm(tempDir, { recursive: true, force: true });
338
+ }
339
+ };
340
+ var readAiDatasetRows = async (params) => {
341
+ const { filePath, limitFromEnd = 0, skipFromEnd = 0 } = params;
342
+ const rows = [];
343
+ const recentLines = [];
344
+ let totalRows = 0;
345
+ const recentWindowLimit = limitFromEnd > 0 ? limitFromEnd + Math.max(0, skipFromEnd) : 0;
346
+ const reader = readline.createInterface({
347
+ input: createReadStream(filePath, { encoding: "utf8" }),
348
+ crlfDelay: Infinity
349
+ });
350
+ for await (const line of reader) {
351
+ const trimmed = line.trim();
352
+ if (!trimmed) {
353
+ continue;
354
+ }
355
+ totalRows += 1;
356
+ if (limitFromEnd > 0) {
357
+ if (recentLines.length === recentWindowLimit) {
358
+ recentLines.shift();
359
+ }
360
+ recentLines.push(trimmed);
361
+ } else {
362
+ const row = JSON.parse(trimmed);
363
+ rows.push(row);
364
+ }
365
+ }
366
+ const effectiveSkip = Math.max(0, skipFromEnd);
367
+ const selectedRecentLines = effectiveSkip > 0 ? recentLines.slice(0, Math.max(0, recentLines.length - effectiveSkip)) : recentLines;
368
+ const selectedRows = limitFromEnd > 0 ? selectedRecentLines.map((line) => JSON.parse(line)) : effectiveSkip > 0 ? rows.slice(0, Math.max(0, rows.length - effectiveSkip)) : rows;
369
+ return {
370
+ rows: selectedRows,
371
+ totalRows
372
+ };
373
+ };
374
+ export {
375
+ appendAiDatasetRow,
376
+ closeAiDatasetWriter,
377
+ closeAllAiDatasetWriters,
378
+ flushAiDatasetWriter,
379
+ getAiChunkFilePath,
380
+ listAiChunkFiles,
381
+ listAiChunkStrategies,
382
+ mergeAiJsonlFiles,
383
+ mergeJsonlFiles,
384
+ readAiDatasetRows,
385
+ toFileToken
386
+ };
@@ -0,0 +1,297 @@
1
+ // src/redis.ts
2
+ import Redis from "ioredis";
3
+ var TTL_1D = 86400;
4
+ var toJson = (value) => JSON.stringify(value);
5
+ var logger = {
6
+ log: (level, message, ...args) => {
7
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : "log";
8
+ console[method](`[infra:redis] ${message}`, ...args);
9
+ }
10
+ };
11
+ var redisConnectionWarningShown = false;
12
+ var redisUnavailable = false;
13
+ var isRedisConnectivityError = (error) => /ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|MaxRetriesPerRequestError|Connection is closed|Stream isn't writeable/i.test(
14
+ error.message
15
+ );
16
+ var toNonNegativeInt = (value, fallback) => {
17
+ const parsed = Number.parseInt(String(value ?? ""), 10);
18
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
19
+ };
20
+ var toPositiveInt = (value, fallback) => {
21
+ const parsed = Number.parseInt(String(value ?? ""), 10);
22
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
23
+ };
24
+ var markRedisUnavailable = (error) => {
25
+ redisUnavailable = true;
26
+ if (redisConnectionWarningShown) return;
27
+ redisConnectionWarningShown = true;
28
+ logger.log(
29
+ "warn",
30
+ "Redis is unavailable: %s. Cache-dependent features are temporarily disabled.",
31
+ error.message
32
+ );
33
+ };
34
+ var getRedis = () => {
35
+ if (!global.__redis__) {
36
+ const host = process.env.REDIS_HOST || "127.0.0.1";
37
+ const port = toPositiveInt(process.env.REDIS_PORT, 6379);
38
+ const connectTimeout = toPositiveInt(
39
+ process.env.REDIS_CONNECT_TIMEOUT_MS,
40
+ 3e3
41
+ );
42
+ const maxRetriesPerRequest = toNonNegativeInt(
43
+ process.env.REDIS_MAX_RETRIES_PER_REQUEST,
44
+ 1
45
+ );
46
+ global.__redis__ = new Redis({
47
+ host,
48
+ port,
49
+ connectTimeout,
50
+ maxRetriesPerRequest,
51
+ enableOfflineQueue: false,
52
+ retryStrategy: (attempt) => Math.min(attempt * 200, 2e3)
53
+ });
54
+ global.__redis__.on("error", (error) => {
55
+ if (isRedisConnectivityError(error)) {
56
+ markRedisUnavailable(error);
57
+ return;
58
+ }
59
+ logger.log("error", "Redis client error: %s", String(error));
60
+ });
61
+ global.__redis__.on("ready", () => {
62
+ redisUnavailable = false;
63
+ if (redisConnectionWarningShown) {
64
+ redisConnectionWarningShown = false;
65
+ logger.log("info", "Redis connection restored");
66
+ }
67
+ });
68
+ }
69
+ return global.__redis__;
70
+ };
71
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
72
+ var getRedisStatus = (redis) => String(redis.status ?? "ready");
73
+ var waitForRedisReady = async (redis) => {
74
+ if (getRedisStatus(redis) === "ready") {
75
+ return true;
76
+ }
77
+ const readyTimeoutMs = toPositiveInt(
78
+ process.env.REDIS_READY_TIMEOUT_MS,
79
+ toPositiveInt(process.env.REDIS_CONNECT_TIMEOUT_MS, 3e3)
80
+ );
81
+ const startedAt = Date.now();
82
+ while (Date.now() - startedAt < readyTimeoutMs) {
83
+ if (getRedisStatus(redis) === "ready") {
84
+ return true;
85
+ }
86
+ if (getRedisStatus(redis) === "end") {
87
+ return false;
88
+ }
89
+ await sleep(50);
90
+ }
91
+ return getRedisStatus(redis) === "ready";
92
+ };
93
+ var getReadyRedis = async () => {
94
+ if (redisUnavailable) return null;
95
+ const redis = getRedis();
96
+ const ready = await waitForRedisReady(redis);
97
+ if (ready) {
98
+ return redis;
99
+ }
100
+ markRedisUnavailable(
101
+ new Error(`Redis is not ready (status=${getRedisStatus(redis)})`)
102
+ );
103
+ return null;
104
+ };
105
+ var toResultString = (result) => {
106
+ if (result == null) return null;
107
+ if (typeof result === "string") return result;
108
+ if (Buffer.isBuffer(result)) return result.toString("utf8");
109
+ return String(result);
110
+ };
111
+ var DEFAULT_OPTIONS = {
112
+ expire: TTL_1D
113
+ };
114
+ var parseJsonOrDeleteKey = async (redis, key, raw, fallback) => {
115
+ try {
116
+ return JSON.parse(raw);
117
+ } catch (e) {
118
+ logger.log("error", "failed JSON.parse(%s): %s", key, String(e));
119
+ await redis.del(key);
120
+ return fallback;
121
+ }
122
+ };
123
+ var getKeys = async (prefix) => {
124
+ if (redisUnavailable) return [];
125
+ const redis = await getReadyRedis();
126
+ if (!redis) return [];
127
+ const keys = [];
128
+ try {
129
+ let cursor = "0";
130
+ do {
131
+ const [nextCursor, batch] = await redis.scan(
132
+ cursor,
133
+ "MATCH",
134
+ `${prefix}*`,
135
+ "COUNT",
136
+ "200"
137
+ );
138
+ cursor = nextCursor;
139
+ for (const key of batch) {
140
+ if (key.startsWith(prefix)) {
141
+ keys.push(key);
142
+ }
143
+ }
144
+ } while (cursor !== "0");
145
+ } catch (e) {
146
+ if (e instanceof Error && isRedisConnectivityError(e)) {
147
+ markRedisUnavailable(e);
148
+ return [];
149
+ }
150
+ logger.log("warn", "failed SCAN for %s: %s", prefix, String(e));
151
+ return [];
152
+ }
153
+ return keys;
154
+ };
155
+ var getData = async (key, fallback = []) => {
156
+ if (redisUnavailable) return fallback;
157
+ const redis = await getReadyRedis();
158
+ if (!redis) return fallback;
159
+ try {
160
+ const rawJson = await redis.call("JSON.GET", key);
161
+ const raw = toResultString(rawJson);
162
+ if (raw == null) return fallback;
163
+ return parseJsonOrDeleteKey(redis, key, raw, fallback);
164
+ } catch (e) {
165
+ if (e instanceof Error && isRedisConnectivityError(e)) {
166
+ markRedisUnavailable(e);
167
+ return fallback;
168
+ }
169
+ logger.log(
170
+ "error",
171
+ "failed JSON.GET %s: %s (fallback to GET)",
172
+ key,
173
+ String(e)
174
+ );
175
+ }
176
+ try {
177
+ const raw = await redis.get(key);
178
+ if (raw == null) return fallback;
179
+ return parseJsonOrDeleteKey(redis, key, raw, fallback);
180
+ } catch (e) {
181
+ if (e instanceof Error && isRedisConnectivityError(e)) {
182
+ markRedisUnavailable(e);
183
+ return fallback;
184
+ }
185
+ logger.log("error", "failed GET %s: %s", key, String(e));
186
+ return fallback;
187
+ }
188
+ };
189
+ var delKey = async (key) => {
190
+ return delKeyWithOptions(key);
191
+ };
192
+ var RedisWriteBlockedError = class extends Error {
193
+ constructor(message) {
194
+ super(message);
195
+ this.name = "RedisWriteBlockedError";
196
+ }
197
+ };
198
+ var delKeyWithOptions = async (key, options = {}) => {
199
+ if (redisUnavailable) return false;
200
+ const { raiseOnMisconf = false } = options;
201
+ const redis = await getReadyRedis();
202
+ if (!redis) return false;
203
+ try {
204
+ const result = await redis.del(key);
205
+ if (result === 1) {
206
+ return true;
207
+ }
208
+ return false;
209
+ } catch (e) {
210
+ if (e instanceof Error && isRedisConnectivityError(e)) {
211
+ markRedisUnavailable(e);
212
+ return false;
213
+ }
214
+ const msg = String(e);
215
+ if (raiseOnMisconf && msg.includes("MISCONF")) {
216
+ throw new RedisWriteBlockedError(msg);
217
+ }
218
+ logger.log("error", "failed DEL %s: %s", key, String(e));
219
+ return false;
220
+ }
221
+ };
222
+ var setData = async (key, data, options = {}) => {
223
+ if (redisUnavailable) return;
224
+ const { expire } = { ...DEFAULT_OPTIONS, ...options };
225
+ const redis = await getReadyRedis();
226
+ if (!redis) return;
227
+ const value = toJson(data);
228
+ try {
229
+ await redis.call("JSON.SET", key, "$", value);
230
+ if (expire) {
231
+ await redis.expire(key, expire);
232
+ }
233
+ } catch (e) {
234
+ if (e instanceof Error && isRedisConnectivityError(e)) {
235
+ markRedisUnavailable(e);
236
+ return;
237
+ }
238
+ logger.log(
239
+ "error",
240
+ "failed JSON.SET %s: %s (fallback to SET)",
241
+ key,
242
+ String(e)
243
+ );
244
+ try {
245
+ if (expire) {
246
+ await redis.set(key, value, "EX", expire);
247
+ } else {
248
+ await redis.set(key, value);
249
+ }
250
+ } catch (e2) {
251
+ if (e2 instanceof Error && isRedisConnectivityError(e2)) {
252
+ markRedisUnavailable(e2);
253
+ return;
254
+ }
255
+ logger.log("error", "failed SET %s: %s", key, String(e2));
256
+ }
257
+ }
258
+ };
259
+ var redisKeys = {
260
+ users: () => "users:index:",
261
+ user: (userName) => `users:index:${userName}`,
262
+ bots: (userName) => `users:${userName}:bots`,
263
+ botsPrefix: () => "users:",
264
+ bot: (userName, botId) => `users:${userName}:bots:${botId}`,
265
+ backtestConfig: (userName, config) => `users:${userName}:backtests:configs:${config}`,
266
+ strategies: (userName) => `users:${userName}:strategies`,
267
+ strategyConfig: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:config`,
268
+ strategyResults: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:results`,
269
+ tests: (userName, strategyName) => strategyName ? `users:${userName}:tests:${strategyName}` : `users:${userName}:tests:`,
270
+ testOrders: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:orders`,
271
+ testConfig: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:config`,
272
+ testStat: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:stat`,
273
+ cacheChunk: (userName, chunkId) => `users:${userName}:cache:tests:chunks:${chunkId}`,
274
+ cacheOrders: (userName, orderLogId) => `users:${userName}:cache:tests:orders:${orderLogId}`,
275
+ cachePositions: (userName, orderLogId) => `users:${userName}:cache:tests:positions:${orderLogId}`,
276
+ signal: (symbol, signalId) => `signals:${symbol}:${signalId}`,
277
+ signalsBySymbol: (symbol) => `signals:${symbol}:`,
278
+ storeSignal: (symbol, signalId) => `store:signals:${symbol}:${signalId}`,
279
+ analysis: (symbol, signalId) => `analysis:${symbol}:${signalId}`,
280
+ backtestResults: (userName, config, timestamp) => `users:${userName}:backtests:results:${config}:${timestamp}`,
281
+ mlSignalsByStrategy: (strategyName) => `ml:${strategyName}:signals:`,
282
+ mlSignals: () => "ml:",
283
+ mlSignal: (strategyName, signalId) => `ml:${strategyName}:signals:${signalId}`,
284
+ mlResultsByStrategy: (strategyName) => `ml:${strategyName}:results:`,
285
+ mlResults: () => "ml:",
286
+ mlResult: (strategyName, signalId) => `ml:${strategyName}:results:${signalId}`
287
+ };
288
+
289
+ export {
290
+ getKeys,
291
+ getData,
292
+ delKey,
293
+ RedisWriteBlockedError,
294
+ delKeyWithOptions,
295
+ setData,
296
+ redisKeys
297
+ };