@tradejs/infra 1.0.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/dist/ml.js ADDED
@@ -0,0 +1,1604 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/ml.ts
31
+ var ml_exports = {};
32
+ __export(ml_exports, {
33
+ analyzeMlSeriesWindow: () => analyzeMlSeriesWindow,
34
+ appendMlDatasetRow: () => appendMlDatasetRow,
35
+ buildMlFeatures: () => buildMlFeatures,
36
+ buildMlSeriesAlignment: () => buildMlSeriesAlignment,
37
+ buildMlTrainingRow: () => buildMlTrainingRow,
38
+ closeAllMlDatasetWriters: () => closeAllMlDatasetWriters,
39
+ closeMlDatasetWriter: () => closeMlDatasetWriter,
40
+ computeWindowBoundaries: () => computeWindowBoundaries,
41
+ fetchMlThreshold: () => fetchMlThreshold,
42
+ findLookaheadViolations: () => findLookaheadViolations,
43
+ flushMlDatasetWriter: () => flushMlDatasetWriter,
44
+ getMlChunkFilePath: () => getMlChunkFilePath,
45
+ isDerivedDatasetFileName: () => isDerivedDatasetFileName,
46
+ isTimestampFeatureKey: () => isTimestampFeatureKey,
47
+ listMlChunkFiles: () => listMlChunkFiles,
48
+ mergeJsonlFiles: () => mergeJsonlFiles,
49
+ toFileToken: () => toFileToken,
50
+ toIsoUtcOrNull: () => toIsoUtcOrNull,
51
+ trimMlTrainingRowWindows: () => trimMlTrainingRowWindows
52
+ });
53
+ module.exports = __toCommonJS(ml_exports);
54
+
55
+ // src/mlDatasetFile.ts
56
+ var import_events = require("events");
57
+ var import_fs = require("fs");
58
+ var import_promises = __toESM(require("fs/promises"));
59
+ var import_path = __toESM(require("path"));
60
+ var DEFAULT_DIR = "data/ml/export";
61
+ var ML_DATASET_WRITE_BATCH_SIZE = 200;
62
+ var writerByPath = /* @__PURE__ */ new Map();
63
+ var toFileToken = (value) => value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "any";
64
+ var getMlChunkFilePath = (strategyName, chunkId, outDir = DEFAULT_DIR) => import_path.default.join(
65
+ outDir,
66
+ `ml-dataset-${toFileToken(strategyName)}-chunk-${toFileToken(chunkId)}.jsonl`
67
+ );
68
+ var appendMlDatasetRow = async (params) => {
69
+ const { strategyName, chunkId, row, outDir = DEFAULT_DIR } = params;
70
+ const filePath = getMlChunkFilePath(strategyName, chunkId, outDir);
71
+ let state = writerByPath.get(filePath);
72
+ if (!state) {
73
+ await import_promises.default.mkdir(outDir, { recursive: true });
74
+ const stream = (0, import_fs.createWriteStream)(filePath, {
75
+ encoding: "utf8",
76
+ flags: "a"
77
+ });
78
+ state = {
79
+ filePath,
80
+ stream,
81
+ buffer: [],
82
+ writeQueue: Promise.resolve(),
83
+ closed: false
84
+ };
85
+ writerByPath.set(filePath, state);
86
+ }
87
+ if (state.closed) {
88
+ throw new Error(`ML dataset writer is closed: ${filePath}`);
89
+ }
90
+ state.buffer.push(`${JSON.stringify(row)}
91
+ `);
92
+ if (state.buffer.length >= ML_DATASET_WRITE_BATCH_SIZE) {
93
+ await flushMlDatasetWriter(filePath);
94
+ }
95
+ return filePath;
96
+ };
97
+ var flushState = async (state) => {
98
+ if (state.closed || state.buffer.length === 0) {
99
+ return;
100
+ }
101
+ const chunk = state.buffer.join("");
102
+ state.buffer = [];
103
+ if (!state.stream.write(chunk)) {
104
+ await (0, import_events.once)(state.stream, "drain");
105
+ }
106
+ };
107
+ var flushMlDatasetWriter = async (filePath) => {
108
+ const state = writerByPath.get(filePath);
109
+ if (!state || state.closed) {
110
+ return;
111
+ }
112
+ state.writeQueue = state.writeQueue.then(() => flushState(state));
113
+ await state.writeQueue;
114
+ };
115
+ var closeState = async (state) => {
116
+ if (state.closed) {
117
+ return;
118
+ }
119
+ await flushState(state);
120
+ state.closed = true;
121
+ state.stream.end();
122
+ await Promise.all([
123
+ (0, import_events.once)(state.stream, "finish"),
124
+ (0, import_events.once)(state.stream, "close")
125
+ ]);
126
+ };
127
+ var closeMlDatasetWriter = async (filePath) => {
128
+ const state = writerByPath.get(filePath);
129
+ if (!state) return;
130
+ state.writeQueue = state.writeQueue.then(() => closeState(state));
131
+ await state.writeQueue;
132
+ writerByPath.delete(filePath);
133
+ };
134
+ var closeAllMlDatasetWriters = async () => {
135
+ const filePaths = [...writerByPath.keys()];
136
+ for (const filePath of filePaths) {
137
+ await closeMlDatasetWriter(filePath);
138
+ }
139
+ };
140
+ var listMlChunkFiles = async (params) => {
141
+ const { strategyName, outDir = DEFAULT_DIR } = params;
142
+ const prefix = `ml-dataset-${toFileToken(strategyName)}-chunk-`;
143
+ let entries = [];
144
+ try {
145
+ entries = await import_promises.default.readdir(outDir);
146
+ } catch (error) {
147
+ return [];
148
+ }
149
+ return entries.filter((name) => name.startsWith(prefix) && name.endsWith(".jsonl")).map((name) => import_path.default.join(outDir, name)).sort();
150
+ };
151
+ var mergeJsonlFiles = async (params) => {
152
+ const { filePaths, outPath } = params;
153
+ await import_promises.default.mkdir(import_path.default.dirname(outPath), { recursive: true });
154
+ const stream = (0, import_fs.createWriteStream)(outPath, { encoding: "utf8" });
155
+ const done = Promise.all([(0, import_events.once)(stream, "finish"), (0, import_events.once)(stream, "close")]);
156
+ try {
157
+ for (const filePath of filePaths) {
158
+ const reader = (0, import_fs.createReadStream)(filePath, { encoding: "utf8" });
159
+ for await (const chunk of reader) {
160
+ if (!stream.write(chunk)) {
161
+ await (0, import_events.once)(stream, "drain");
162
+ }
163
+ }
164
+ }
165
+ } finally {
166
+ stream.end();
167
+ await done;
168
+ }
169
+ };
170
+
171
+ // src/mlGrpc.ts
172
+ var import_path2 = __toESM(require("path"));
173
+ var grpc = __toESM(require("@grpc/grpc-js"));
174
+ var protoLoader = __toESM(require("@grpc/proto-loader"));
175
+ var import_fs2 = __toESM(require("fs"));
176
+
177
+ // src/logger.ts
178
+ var import_winston = require("winston");
179
+ var baseFormat = import_winston.format.combine(
180
+ import_winston.format.timestamp({ format: "DD MMM HH:mm:ss" }),
181
+ import_winston.format.splat()
182
+ );
183
+ var logger = (0, import_winston.createLogger)({
184
+ format: baseFormat,
185
+ transports: [
186
+ new import_winston.transports.Console({
187
+ format: import_winston.format.combine(
188
+ import_winston.format.colorize({ all: true }),
189
+ import_winston.format.printf(
190
+ ({ level, timestamp, message }) => `${level}: ${timestamp}: ${message}`
191
+ )
192
+ )
193
+ }),
194
+ new import_winston.transports.File({
195
+ filename: "service.log",
196
+ format: import_winston.format.combine(
197
+ import_winston.format.uncolorize(),
198
+ import_winston.format.printf(
199
+ ({ level, timestamp, message }) => `${level}: ${timestamp}: ${message}`
200
+ )
201
+ )
202
+ }),
203
+ new import_winston.transports.File({
204
+ filename: "error.log",
205
+ format: import_winston.format.combine(
206
+ import_winston.format.uncolorize(),
207
+ import_winston.format.printf(
208
+ ({ level, timestamp, message }) => `${level}: ${timestamp}: ${message}`
209
+ )
210
+ ),
211
+ level: "error"
212
+ })
213
+ ]
214
+ });
215
+
216
+ // src/mlGrpc.ts
217
+ var clientCache = /* @__PURE__ */ new Map();
218
+ var resolveProtoPath = () => {
219
+ const candidates = [
220
+ import_path2.default.resolve(__dirname, "../proto/ml_infer.proto"),
221
+ import_path2.default.resolve(__dirname, "../../proto/ml_infer.proto"),
222
+ import_path2.default.resolve(process.cwd(), "proto/ml_infer.proto")
223
+ ];
224
+ for (const candidate of candidates) {
225
+ if (import_fs2.default.existsSync(candidate)) {
226
+ return candidate;
227
+ }
228
+ }
229
+ return candidates[candidates.length - 1];
230
+ };
231
+ var getClient = (address) => {
232
+ const cached = clientCache.get(address);
233
+ if (cached) return cached;
234
+ const protoPath = resolveProtoPath();
235
+ const packageDefinition = protoLoader.loadSync(protoPath, {
236
+ keepCase: true,
237
+ longs: String,
238
+ enums: String,
239
+ defaults: true,
240
+ oneofs: true
241
+ });
242
+ const proto = grpc.loadPackageDefinition(packageDefinition);
243
+ const client = new proto.ml_infer.MlInfer(
244
+ address,
245
+ grpc.credentials.createInsecure()
246
+ );
247
+ clientCache.set(address, client);
248
+ return client;
249
+ };
250
+ var toFiniteNumber = (value) => {
251
+ if (typeof value === "number" && Number.isFinite(value)) return value;
252
+ const parsed = Number(value);
253
+ return Number.isFinite(parsed) ? parsed : null;
254
+ };
255
+ var buildMlFeatures = (row) => {
256
+ const features = {};
257
+ for (const [key, value] of Object.entries(row)) {
258
+ if (key === "label" || key === "profit" || key === "entryTimestamp" || key === "strategy") {
259
+ continue;
260
+ }
261
+ const num = toFiniteNumber(value);
262
+ if (num != null) {
263
+ features[key] = num;
264
+ }
265
+ }
266
+ return features;
267
+ };
268
+ var fetchMlThreshold = async ({
269
+ strategy,
270
+ features,
271
+ threshold,
272
+ grpcAddress
273
+ }) => {
274
+ try {
275
+ const address = grpcAddress || process.env.ML_GRPC_ADDRESS || "localhost:50051";
276
+ const client = getClient(address);
277
+ return await new Promise((resolve, reject) => {
278
+ client.Predict(
279
+ {
280
+ strategy,
281
+ features,
282
+ threshold
283
+ },
284
+ (err, response) => {
285
+ if (err) {
286
+ reject(err);
287
+ return;
288
+ }
289
+ resolve(response);
290
+ }
291
+ );
292
+ });
293
+ } catch (err) {
294
+ logger.error("ml grpc error: %s", err);
295
+ return null;
296
+ }
297
+ };
298
+
299
+ // src/mlSeriesAnalysis.ts
300
+ var toFinite = (value, fallback = 0) => Number.isFinite(value) ? value : fallback;
301
+ var safeDiv = (num, denom) => {
302
+ if (!Number.isFinite(num) || !Number.isFinite(denom) || denom === 0) return 0;
303
+ return num / denom;
304
+ };
305
+ var clamp = (value, min, max) => {
306
+ if (!Number.isFinite(value)) return 0;
307
+ if (value < min) return min;
308
+ if (value > max) return max;
309
+ return value;
310
+ };
311
+ var mean = (values) => {
312
+ if (!values.length) return 0;
313
+ return values.reduce((acc, value) => acc + value, 0) / values.length;
314
+ };
315
+ var std = (values) => {
316
+ if (!values.length) return 0;
317
+ const valuesMean = mean(values);
318
+ const variance = values.reduce((acc, value) => acc + (value - valuesMean) ** 2, 0) / values.length;
319
+ return Math.sqrt(variance);
320
+ };
321
+ var median = (values) => {
322
+ if (!values.length) return 0;
323
+ const sorted = [...values].sort((a, b) => a - b);
324
+ const mid = Math.floor(sorted.length / 2);
325
+ if (sorted.length % 2 === 0) {
326
+ return (sorted[mid - 1] + sorted[mid]) / 2;
327
+ }
328
+ return sorted[mid];
329
+ };
330
+ var percentileRank = (values, target) => {
331
+ if (!values.length || !Number.isFinite(target)) return 0.5;
332
+ let less = 0;
333
+ let equal = 0;
334
+ for (const value of values) {
335
+ if (!Number.isFinite(value)) continue;
336
+ if (value < target) less += 1;
337
+ else if (value === target) equal += 1;
338
+ }
339
+ return clamp((less + equal * 0.5) / values.length, 0, 1);
340
+ };
341
+ var linearSlope = (values) => {
342
+ const n = values.length;
343
+ if (n < 2) return 0;
344
+ const xMean = (n - 1) / 2;
345
+ const yMean = mean(values);
346
+ let cov = 0;
347
+ let varX = 0;
348
+ for (let i = 0; i < n; i += 1) {
349
+ const dx = i - xMean;
350
+ cov += dx * (values[i] - yMean);
351
+ varX += dx * dx;
352
+ }
353
+ return varX === 0 ? 0 : cov / varX;
354
+ };
355
+ var simpleReturns = (values) => {
356
+ if (values.length < 2) return [];
357
+ const result = [];
358
+ for (let i = 1; i < values.length; i += 1) {
359
+ result.push(clamp(safeDiv(values[i], values[i - 1]) - 1, -5, 5));
360
+ }
361
+ return result;
362
+ };
363
+ var sign = (value) => {
364
+ if (value > 0) return 1;
365
+ if (value < 0) return -1;
366
+ return 0;
367
+ };
368
+ var normalizeCandle = (candle) => ({
369
+ open: toFinite(Number(candle?.open ?? 0), 0),
370
+ high: toFinite(Number(candle?.high ?? 0), 0),
371
+ low: toFinite(Number(candle?.low ?? 0), 0),
372
+ close: toFinite(Number(candle?.close ?? 0), 0),
373
+ volume: toFinite(Number(candle?.volume ?? 0), 0),
374
+ timestamp: toFinite(Number(candle?.timestamp ?? 0), 0)
375
+ });
376
+ var analyzeNumericSeries = (values) => {
377
+ const clean = values.map((value) => toFinite(value, 0));
378
+ const last = clean[clean.length - 1] ?? 0;
379
+ const valuesMean = mean(clean);
380
+ const valuesStd = std(clean);
381
+ const slope = linearSlope(clean);
382
+ const zLast = valuesStd > 0 ? (last - valuesMean) / valuesStd : 0;
383
+ return {
384
+ LAST: last,
385
+ MEAN: valuesMean,
386
+ STD: valuesStd,
387
+ SLOPE: slope,
388
+ SLOPE_NORM: Math.tanh(safeDiv(slope, Math.abs(valuesMean) + 1e-9)),
389
+ Z_LAST: clamp(zLast, -8, 8),
390
+ RANK_LAST: percentileRank(clean.length ? clean : [0], last)
391
+ };
392
+ };
393
+ var candleCoreSummary = (rawCandles) => {
394
+ const candles = rawCandles.map((candle) => normalizeCandle(candle));
395
+ if (!candles.length) {
396
+ return {
397
+ CLOSE_NET_RET: 0,
398
+ CLOSE_SLOPE_NORM: 0,
399
+ CLOSE_VOL: 0,
400
+ TREND_EFFICIENCY: 0,
401
+ UP_MOVE_RATIO: 0,
402
+ AVG_RANGE_PCT: 0,
403
+ AVG_BODY_PCT: 0,
404
+ WICK_IMBALANCE: 0,
405
+ RANGE_POSITION: 0.5,
406
+ DRAWDOWN_FROM_HIGH: 0,
407
+ REBOUND_FROM_LOW: 0,
408
+ VOLUME_SLOPE_NORM: 0,
409
+ VOLUME_SPIKE: 0,
410
+ BREAKOUT_ABOVE_PREV_HIGH: 0,
411
+ BREAKOUT_BELOW_PREV_LOW: 0
412
+ };
413
+ }
414
+ const closes = candles.map((candle) => toFinite(candle.close, 0));
415
+ const opens = candles.map((candle) => toFinite(candle.open, 0));
416
+ const highs = candles.map((candle) => toFinite(candle.high, 0));
417
+ const lows = candles.map((candle) => toFinite(candle.low, 0));
418
+ const volumes = candles.map(
419
+ (candle) => Math.log1p(Math.max(0, candle.volume))
420
+ );
421
+ const rangesPct = candles.map(
422
+ (candle) => safeDiv(
423
+ Math.max(0, candle.high - candle.low),
424
+ Math.abs(candle.close) + 1e-9
425
+ )
426
+ );
427
+ const bodyPct = candles.map(
428
+ (candle) => safeDiv(
429
+ Math.abs(candle.close - candle.open),
430
+ Math.abs(candle.close) + 1e-9
431
+ )
432
+ );
433
+ const wickImbalance = candles.map((candle) => {
434
+ const range = Math.max(0, candle.high - candle.low);
435
+ if (range === 0) return 0;
436
+ const upper = candle.high - Math.max(candle.open, candle.close);
437
+ const lower = Math.min(candle.open, candle.close) - candle.low;
438
+ return clamp(safeDiv(upper - lower, range), -1, 1);
439
+ });
440
+ const closeReturns = simpleReturns(closes);
441
+ let upMoves = 0;
442
+ let absMoveSum = 0;
443
+ for (let i = 1; i < closes.length; i += 1) {
444
+ const diff = closes[i] - closes[i - 1];
445
+ if (diff > 0) upMoves += 1;
446
+ absMoveSum += Math.abs(diff);
447
+ }
448
+ const firstClose = closes[0] ?? 0;
449
+ const lastClose = closes[closes.length - 1] ?? 0;
450
+ const netMove = lastClose - firstClose;
451
+ const closeSlope = linearSlope(closes);
452
+ const highMax = highs.length ? Math.max(...highs) : 0;
453
+ const lowMin = lows.length ? Math.min(...lows) : 0;
454
+ const windowRange = Math.max(0, highMax - lowMin);
455
+ const prevHigh = highs.length > 1 ? Math.max(...highs.slice(0, highs.length - 1)) : highMax;
456
+ const prevLow = lows.length > 1 ? Math.min(...lows.slice(0, lows.length - 1)) : lowMin;
457
+ return {
458
+ CLOSE_NET_RET: clamp(safeDiv(lastClose, firstClose) - 1, -5, 5),
459
+ CLOSE_SLOPE_NORM: Math.tanh(
460
+ safeDiv(closeSlope, Math.abs(firstClose) + 1e-9)
461
+ ),
462
+ CLOSE_VOL: std(closeReturns),
463
+ TREND_EFFICIENCY: clamp(safeDiv(Math.abs(netMove), absMoveSum), 0, 1),
464
+ UP_MOVE_RATIO: clamp(
465
+ safeDiv(upMoves, Math.max(1, closes.length - 1)),
466
+ 0,
467
+ 1
468
+ ),
469
+ AVG_RANGE_PCT: clamp(mean(rangesPct), 0, 5),
470
+ AVG_BODY_PCT: clamp(mean(bodyPct), 0, 5),
471
+ WICK_IMBALANCE: mean(wickImbalance),
472
+ RANGE_POSITION: clamp(safeDiv(lastClose - lowMin, windowRange || 1), 0, 1),
473
+ DRAWDOWN_FROM_HIGH: clamp(
474
+ safeDiv(highMax - lastClose, Math.abs(lastClose) + 1e-9),
475
+ 0,
476
+ 5
477
+ ),
478
+ REBOUND_FROM_LOW: clamp(
479
+ safeDiv(lastClose - lowMin, Math.abs(lastClose) + 1e-9),
480
+ -5,
481
+ 5
482
+ ),
483
+ VOLUME_SLOPE_NORM: Math.tanh(linearSlope(volumes)),
484
+ VOLUME_SPIKE: clamp(
485
+ safeDiv(volumes[volumes.length - 1] ?? 0, median(volumes) || 1),
486
+ 0,
487
+ 10
488
+ ),
489
+ BREAKOUT_ABOVE_PREV_HIGH: clamp(
490
+ safeDiv(lastClose - prevHigh, Math.abs(lastClose) + 1e-9),
491
+ -5,
492
+ 5
493
+ ),
494
+ BREAKOUT_BELOW_PREV_LOW: clamp(
495
+ safeDiv(prevLow - lastClose, Math.abs(lastClose) + 1e-9),
496
+ -5,
497
+ 5
498
+ )
499
+ };
500
+ };
501
+ var analyzeMlSeriesWindow = (input) => {
502
+ const summary = {
503
+ ...candleCoreSummary(input.candles)
504
+ };
505
+ const benchmark = input.benchmarkCandles;
506
+ if (benchmark?.length) {
507
+ const benchSummary = candleCoreSummary(benchmark);
508
+ const closeNetRet = summary.CLOSE_NET_RET ?? 0;
509
+ const benchNetRet = benchSummary.CLOSE_NET_RET ?? 0;
510
+ const closeSlopeNorm = summary.CLOSE_SLOPE_NORM ?? 0;
511
+ const benchSlopeNorm = benchSummary.CLOSE_SLOPE_NORM ?? 0;
512
+ summary.REL_BENCH_NET_RET = clamp(closeNetRet - benchNetRet, -5, 5);
513
+ summary.REL_BENCH_SLOPE_GAP = clamp(closeSlopeNorm - benchSlopeNorm, -2, 2);
514
+ summary.REL_BENCH_TREND_ALIGN = clamp(
515
+ sign(closeSlopeNorm) * sign(benchSlopeNorm),
516
+ -1,
517
+ 1
518
+ );
519
+ summary.REL_BENCH_VOL_RATIO = clamp(
520
+ safeDiv(summary.CLOSE_VOL ?? 0, benchSummary.CLOSE_VOL ?? 0),
521
+ 0,
522
+ 10
523
+ );
524
+ }
525
+ const indicatorMap = input.indicators;
526
+ if (indicatorMap) {
527
+ const indicatorEntries = [
528
+ ["ATR_PCT", indicatorMap.atrPct],
529
+ ["PRICE1H_PCNT", indicatorMap.price1hPcnt],
530
+ ["PRICE24H_PCNT", indicatorMap.price24hPcnt],
531
+ ["MACD_HIST", indicatorMap.macdHistogram]
532
+ ];
533
+ for (const [prefix, maybeSeries] of indicatorEntries) {
534
+ const series = (maybeSeries ?? []).filter(
535
+ (value) => Number.isFinite(value)
536
+ );
537
+ if (!series.length) continue;
538
+ const stats = analyzeNumericSeries(series);
539
+ summary[`${prefix}_LAST`] = stats.LAST ?? 0;
540
+ summary[`${prefix}_MEAN`] = stats.MEAN ?? 0;
541
+ summary[`${prefix}_STD`] = stats.STD ?? 0;
542
+ summary[`${prefix}_SLOPE_NORM`] = stats.SLOPE_NORM ?? 0;
543
+ summary[`${prefix}_Z_LAST`] = stats.Z_LAST ?? 0;
544
+ summary[`${prefix}_RANK_LAST`] = stats.RANK_LAST ?? 0;
545
+ }
546
+ const maFast = (indicatorMap.maFast ?? []).filter(
547
+ (v) => Number.isFinite(v)
548
+ );
549
+ const maSlow = (indicatorMap.maSlow ?? []).filter(
550
+ (v) => Number.isFinite(v)
551
+ );
552
+ if (maFast.length && maSlow.length) {
553
+ const length = Math.min(maFast.length, maSlow.length);
554
+ const spread = Array.from(
555
+ { length },
556
+ (_, i) => clamp(
557
+ safeDiv(
558
+ maFast[maFast.length - length + i] - maSlow[maSlow.length - length + i],
559
+ maSlow[maSlow.length - length + i] || 1
560
+ ),
561
+ -5,
562
+ 5
563
+ )
564
+ );
565
+ const spreadStats = analyzeNumericSeries(spread);
566
+ summary.MA_FAST_SLOW_SPREAD_LAST = spreadStats.LAST ?? 0;
567
+ summary.MA_FAST_SLOW_SPREAD_SLOPE_NORM = spreadStats.SLOPE_NORM ?? 0;
568
+ summary.MA_FAST_SLOW_SPREAD_Z_LAST = spreadStats.Z_LAST ?? 0;
569
+ }
570
+ }
571
+ return summary;
572
+ };
573
+ var buildMlSeriesAlignment = (left, right) => {
574
+ const leftSlope = left?.CLOSE_SLOPE_NORM ?? 0;
575
+ const rightSlope = right?.CLOSE_SLOPE_NORM ?? 0;
576
+ const leftRet = left?.CLOSE_NET_RET ?? 0;
577
+ const rightRet = right?.CLOSE_NET_RET ?? 0;
578
+ const leftEfficiency = left?.TREND_EFFICIENCY ?? 0;
579
+ const rightEfficiency = right?.TREND_EFFICIENCY ?? 0;
580
+ return {
581
+ TREND_ALIGN_SIGN: clamp(sign(leftSlope) * sign(rightSlope), -1, 1),
582
+ TREND_SLOPE_GAP: clamp(leftSlope - rightSlope, -2, 2),
583
+ NET_RET_GAP: clamp(leftRet - rightRet, -5, 5),
584
+ EFFICIENCY_GAP: clamp(leftEfficiency - rightEfficiency, -1, 1),
585
+ CONFLUENCE_SCORE: clamp(
586
+ leftSlope * rightSlope * (0.5 + 0.5 * Math.min(leftEfficiency, rightEfficiency)),
587
+ -1,
588
+ 1
589
+ )
590
+ };
591
+ };
592
+
593
+ // src/mlTrainingTransform.ts
594
+ var ML_BASE_CANDLES_WINDOW = 50;
595
+ var ML_CANDLE_FEATURE_WINDOW = 50;
596
+ var ML_WINDOW_POLICY = {
597
+ indicatorWindow: Math.max(1, ML_BASE_CANDLES_WINDOW - 1),
598
+ candleWindow: Math.max(1, ML_CANDLE_FEATURE_WINDOW - 1),
599
+ outputWindow: 5,
600
+ dropLastIndicatorElement: true
601
+ };
602
+ var CANDLE_WINDOW = ML_WINDOW_POLICY.candleWindow;
603
+ var INDICATOR_WINDOW = ML_WINDOW_POLICY.indicatorWindow;
604
+ var CANDLE_TIMEFRAMES = [
605
+ { label: "TF15M", key: "candles15m", btcKey: "btcCandles15m" },
606
+ { label: "TF1H", key: "candles1h", btcKey: "btcCandles1h" },
607
+ { label: "TF4H", key: "candles4h", btcKey: "btcCandles4h" },
608
+ { label: "TF1D", key: "candles1d", btcKey: "btcCandles1d" }
609
+ ];
610
+ var INDICATOR_TIMEFRAMES = [
611
+ { label: "TF15M", suffix: "" },
612
+ { label: "TF1H", suffix: "1h" },
613
+ { label: "TF4H", suffix: "4h" },
614
+ { label: "TF1D", suffix: "1d" }
615
+ ];
616
+ var toNumber = (value, fallback = 0) => {
617
+ if (typeof value === "number") {
618
+ return Number.isFinite(value) ? value : fallback;
619
+ }
620
+ const parsed = Number(value);
621
+ return Number.isFinite(parsed) ? parsed : fallback;
622
+ };
623
+ var safeDiv2 = (num, denom) => {
624
+ if (!Number.isFinite(num) || !Number.isFinite(denom) || denom === 0) {
625
+ return 0;
626
+ }
627
+ return num / denom;
628
+ };
629
+ var safeLog1p = (value) => {
630
+ if (!Number.isFinite(value)) {
631
+ return 0;
632
+ }
633
+ if (value === 0) {
634
+ return 0;
635
+ }
636
+ return Math.sign(value) * Math.log1p(Math.abs(value));
637
+ };
638
+ var clamp2 = (value, min, max) => {
639
+ if (!Number.isFinite(value)) return 0;
640
+ if (value < min) return min;
641
+ if (value > max) return max;
642
+ return value;
643
+ };
644
+ var safeLog1pPositive = (value) => {
645
+ if (!Number.isFinite(value) || value <= 0) return 0;
646
+ return Math.log1p(value);
647
+ };
648
+ var squash = (value, scale) => {
649
+ if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0) {
650
+ return 0;
651
+ }
652
+ return Math.tanh(value / scale);
653
+ };
654
+ var normalizeSymbol = (value) => {
655
+ if (typeof value !== "string") return "";
656
+ return value.trim().toUpperCase();
657
+ };
658
+ var computeMedian = (values) => {
659
+ if (values.length === 0) return 0;
660
+ const sorted = [...values].sort((a, b) => a - b);
661
+ const mid = Math.floor(sorted.length / 2);
662
+ if (sorted.length % 2 === 0) {
663
+ return (sorted[mid - 1] + sorted[mid]) / 2;
664
+ }
665
+ return sorted[mid];
666
+ };
667
+ var computeMean = (values) => {
668
+ if (values.length === 0) return 0;
669
+ const sum = values.reduce((acc, value) => acc + value, 0);
670
+ return sum / values.length;
671
+ };
672
+ var computeStd = (values) => {
673
+ if (values.length === 0) return 0;
674
+ const mean2 = computeMean(values);
675
+ const variance = values.reduce((acc, value) => acc + (value - mean2) ** 2, 0) / values.length;
676
+ return Math.sqrt(variance);
677
+ };
678
+ var computeSkew = (values) => {
679
+ if (values.length === 0) return 0;
680
+ const mean2 = computeMean(values);
681
+ const std2 = computeStd(values);
682
+ if (std2 === 0) return 0;
683
+ const m3 = values.reduce((acc, value) => acc + (value - mean2) ** 3, 0) / values.length;
684
+ return m3 / std2 ** 3;
685
+ };
686
+ var computeKurtosis = (values) => {
687
+ if (values.length === 0) return 0;
688
+ const mean2 = computeMean(values);
689
+ const std2 = computeStd(values);
690
+ if (std2 === 0) return 0;
691
+ const m4 = values.reduce((acc, value) => acc + (value - mean2) ** 4, 0) / values.length;
692
+ return m4 / std2 ** 4;
693
+ };
694
+ var percentileRank2 = (values, target) => {
695
+ if (!values.length || !Number.isFinite(target)) return 0.5;
696
+ let less = 0;
697
+ let equal = 0;
698
+ for (const value of values) {
699
+ if (!Number.isFinite(value)) continue;
700
+ if (value < target) less += 1;
701
+ else if (value === target) equal += 1;
702
+ }
703
+ const n = values.length;
704
+ if (n === 0) return 0.5;
705
+ return clamp2((less + equal * 0.5) / n, 0, 1);
706
+ };
707
+ var standardizeSeries = (values) => {
708
+ if (!values.length) return [];
709
+ const valuesMean = computeMean(values);
710
+ const valuesStd = computeStd(values);
711
+ if (valuesStd === 0) {
712
+ return values.map(() => 0);
713
+ }
714
+ return values.map((value) => (value - valuesMean) / valuesStd);
715
+ };
716
+ var backwardReturns = (values) => {
717
+ if (values.length < 2) return [];
718
+ const result = [];
719
+ for (let i = values.length - 1; i >= 1; i -= 1) {
720
+ result.push(clamp2(safeDiv2(values[i], values[i - 1]) - 1, -5, 5));
721
+ }
722
+ return result;
723
+ };
724
+ var assignBackwardReturns = (row, prefix, returnsNewestFirst) => {
725
+ const returnsOldestFirst = [...returnsNewestFirst].reverse();
726
+ for (let i = 0; i < returnsOldestFirst.length; i += 1) {
727
+ row[`${prefix}_${i + 2}`] = clamp2(returnsOldestFirst[i], -5, 5);
728
+ }
729
+ };
730
+ var sliceWindow = (values, endIndex, windowSize) => {
731
+ if (values.length === 0) return [];
732
+ const start = Math.max(0, endIndex - windowSize + 1);
733
+ return values.slice(start, endIndex + 1);
734
+ };
735
+ var asArray = (value) => Array.isArray(value) ? value : [];
736
+ var dropLastFromIndicatorSeries = (indicators) => {
737
+ if (!ML_WINDOW_POLICY.dropLastIndicatorElement) {
738
+ return indicators;
739
+ }
740
+ const next = {};
741
+ for (const [key, value] of Object.entries(indicators)) {
742
+ if (Array.isArray(value)) {
743
+ next[key] = value.slice(0, Math.max(0, value.length - 1));
744
+ } else {
745
+ next[key] = value;
746
+ }
747
+ }
748
+ return next;
749
+ };
750
+ var normalizeSeries = (value) => {
751
+ if (Array.isArray(value)) {
752
+ return value.map((item) => toNumber(item, 0));
753
+ }
754
+ if (typeof value === "number") {
755
+ return [toNumber(value, 0)];
756
+ }
757
+ return [];
758
+ };
759
+ var padSeries = (values) => {
760
+ if (values.length >= INDICATOR_WINDOW) {
761
+ return values.slice(-INDICATOR_WINDOW);
762
+ }
763
+ const fill = values.length ? values[0] : 0;
764
+ const missing = INDICATOR_WINDOW - values.length;
765
+ return Array.from({ length: missing }, () => fill).concat(values);
766
+ };
767
+ var normalizeCandles = (value) => {
768
+ if (!Array.isArray(value)) return [];
769
+ const normalized = value.map((item) => ({
770
+ open: toNumber(item?.open, 0),
771
+ high: toNumber(item?.high, 0),
772
+ low: toNumber(item?.low, 0),
773
+ close: toNumber(item?.close, 0),
774
+ volume: toNumber(item?.volume, 0),
775
+ timestamp: toNumber(item?.timestamp, 0)
776
+ })).filter((candle) => candle.timestamp > 0);
777
+ normalized.sort((a, b) => a.timestamp - b.timestamp);
778
+ return normalized;
779
+ };
780
+ var padCandles = (candles) => {
781
+ if (candles.length >= CANDLE_WINDOW) {
782
+ return candles.slice(-CANDLE_WINDOW);
783
+ }
784
+ const fill = candles[0] ?? {
785
+ open: 0,
786
+ high: 0,
787
+ low: 0,
788
+ close: 0,
789
+ volume: 0,
790
+ timestamp: 0
791
+ };
792
+ const missing = CANDLE_WINDOW - candles.length;
793
+ return Array.from({ length: missing }, () => ({ ...fill })).concat(candles);
794
+ };
795
+ var padNumberWindow = (values, fallback) => {
796
+ if (values.length >= CANDLE_WINDOW) {
797
+ return values.slice(-CANDLE_WINDOW);
798
+ }
799
+ const fill = values.length ? values[0] : fallback;
800
+ const missing = CANDLE_WINDOW - values.length;
801
+ return Array.from({ length: missing }, () => fill).concat(values);
802
+ };
803
+ var addCandleFeatures = (row, params) => {
804
+ const {
805
+ featurePrefix = "",
806
+ candles: rawCandles,
807
+ btcCandles: rawBtcCandles,
808
+ currentPrice,
809
+ priceScaleSeries: rawPriceScaleSeries
810
+ } = params;
811
+ const candles = padCandles(rawCandles);
812
+ const btcCandles = padCandles(rawBtcCandles);
813
+ const priceScaleSeries = padNumberWindow(rawPriceScaleSeries, currentPrice);
814
+ const key = (name) => featurePrefix ? `${featurePrefix}_${name}` : name;
815
+ const altKey = (name) => key(`ALT_${name}`);
816
+ const btcKey = (name) => key(`BTC_${name}`);
817
+ const candleVolumes = candles.map((candle) => candle.volume);
818
+ const btcVolumes = btcCandles.map((candle) => candle.volume);
819
+ const altReturns = [];
820
+ const btcReturns = [];
821
+ const relReturns = [];
822
+ const candleBodyRaw = [];
823
+ const altToBtcOpenRaw = [];
824
+ const altToBtcCloseRaw = [];
825
+ const altToBtcHighRaw = [];
826
+ const altToBtcLowRaw = [];
827
+ const lastBtcClose = toNumber(btcCandles[btcCandles.length - 1]?.close, 0);
828
+ const btcPrice = lastBtcClose;
829
+ for (let i = 0; i < CANDLE_WINDOW; i += 1) {
830
+ const candle = candles[i] ?? {};
831
+ const btcCandle = btcCandles[i] ?? {};
832
+ const altOpenRaw = toNumber(candle?.open, 0);
833
+ const altCloseRaw = toNumber(candle?.close, 0);
834
+ const altHighRaw = toNumber(candle?.high, 0);
835
+ const altLowRaw = toNumber(candle?.low, 0);
836
+ const btcOpenRaw = toNumber(btcCandle?.open, 0);
837
+ const btcCloseRaw = toNumber(btcCandle?.close, 0);
838
+ const btcHighRaw = toNumber(btcCandle?.high, 0);
839
+ const btcLowRaw = toNumber(btcCandle?.low, 0);
840
+ const altRet = safeDiv2(altCloseRaw, altOpenRaw);
841
+ const btcRet = safeDiv2(btcCloseRaw, btcOpenRaw);
842
+ row[altKey(`Ret_${i + 1}`)] = altRet;
843
+ row[btcKey(`Ret_${i + 1}`)] = btcRet;
844
+ row[key(`RelRet_${i + 1}`)] = altRet - btcRet;
845
+ altReturns.push(altRet);
846
+ btcReturns.push(btcRet);
847
+ relReturns.push(altRet - btcRet);
848
+ const altToBtcOpen = safeDiv2(altOpenRaw, btcOpenRaw);
849
+ const altToBtcClose = safeDiv2(altCloseRaw, btcCloseRaw);
850
+ const altToBtcHigh = safeDiv2(altHighRaw, btcHighRaw);
851
+ const altToBtcLow = safeDiv2(altLowRaw, btcLowRaw);
852
+ altToBtcOpenRaw.push(altToBtcOpen);
853
+ altToBtcCloseRaw.push(altToBtcClose);
854
+ altToBtcHighRaw.push(altToBtcHigh);
855
+ altToBtcLowRaw.push(altToBtcLow);
856
+ const priceScale = toNumber(priceScaleSeries[i], currentPrice);
857
+ const candleMax = Math.max(altOpenRaw, altCloseRaw);
858
+ const candleMin = Math.min(altOpenRaw, altCloseRaw);
859
+ candleBodyRaw.push(safeDiv2(altCloseRaw - altOpenRaw, priceScale));
860
+ row[altKey(`Candle_Range_${i + 1}`)] = safeDiv2(
861
+ altHighRaw - altLowRaw,
862
+ priceScale
863
+ );
864
+ row[altKey(`Candle_UpperWick_${i + 1}`)] = safeDiv2(
865
+ altHighRaw - candleMax,
866
+ priceScale
867
+ );
868
+ row[altKey(`Candle_LowerWick_${i + 1}`)] = safeDiv2(
869
+ candleMin - altLowRaw,
870
+ priceScale
871
+ );
872
+ row[altKey(`Candle_Direction_${i + 1}`)] = altCloseRaw >= altOpenRaw ? 1 : 0;
873
+ const btcCandleMax = Math.max(btcOpenRaw, btcCloseRaw);
874
+ const btcCandleMin = Math.min(btcOpenRaw, btcCloseRaw);
875
+ row[btcKey(`Candle_Body_${i + 1}`)] = safeDiv2(
876
+ btcCloseRaw - btcOpenRaw,
877
+ btcPrice
878
+ );
879
+ row[btcKey(`Candle_Range_${i + 1}`)] = safeDiv2(
880
+ btcHighRaw - btcLowRaw,
881
+ btcPrice
882
+ );
883
+ row[btcKey(`Candle_UpperWick_${i + 1}`)] = safeDiv2(
884
+ btcHighRaw - btcCandleMax,
885
+ btcPrice
886
+ );
887
+ row[btcKey(`Candle_LowerWick_${i + 1}`)] = safeDiv2(
888
+ btcCandleMin - btcLowRaw,
889
+ btcPrice
890
+ );
891
+ row[btcKey(`Candle_Direction_${i + 1}`)] = btcCloseRaw >= btcOpenRaw ? 1 : 0;
892
+ const candleVol = toNumber(candle?.volume, 0);
893
+ const btcVol = toNumber(btcCandle?.volume, 0);
894
+ const candleWindow = sliceWindow(
895
+ candleVolumes,
896
+ i,
897
+ Math.min(20, candleVolumes.length)
898
+ );
899
+ const btcWindow = sliceWindow(
900
+ btcVolumes,
901
+ i,
902
+ Math.min(20, btcVolumes.length)
903
+ );
904
+ const candleMedian = computeMedian(candleWindow);
905
+ const btcMedian = computeMedian(btcWindow);
906
+ row[altKey(`Candle_Volume_${i + 1}`)] = safeLog1p(candleVol);
907
+ if (i > 0) {
908
+ row[altKey(`Candle_Volume_${i + 1}_MedianNorm`)] = safeDiv2(
909
+ candleVol,
910
+ candleMedian
911
+ );
912
+ }
913
+ row[btcKey(`Candle_Volume_${i + 1}`)] = safeLog1p(btcVol);
914
+ if (i > 0) {
915
+ row[btcKey(`Candle_Volume_${i + 1}_MedianNorm`)] = safeDiv2(
916
+ btcVol,
917
+ btcMedian
918
+ );
919
+ }
920
+ }
921
+ const candleBodyStd = standardizeSeries(candleBodyRaw);
922
+ const altToBtcOpenRet = backwardReturns(altToBtcOpenRaw);
923
+ const altToBtcCloseRet = backwardReturns(altToBtcCloseRaw);
924
+ const altToBtcHighRet = backwardReturns(altToBtcHighRaw);
925
+ const altToBtcLowRet = backwardReturns(altToBtcLowRaw);
926
+ for (let i = 0; i < CANDLE_WINDOW; i += 1) {
927
+ row[altKey(`Candle_Body_${i + 1}`)] = candleBodyStd[i] ?? 0;
928
+ }
929
+ assignBackwardReturns(row, key("AltToBtc_Open"), altToBtcOpenRet);
930
+ assignBackwardReturns(row, key("AltToBtc_Close"), altToBtcCloseRet);
931
+ assignBackwardReturns(row, key("AltToBtc_High"), altToBtcHighRet);
932
+ assignBackwardReturns(row, key("AltToBtc_Low"), altToBtcLowRet);
933
+ const windowAlt = sliceWindow(
934
+ altReturns,
935
+ altReturns.length - 1,
936
+ CANDLE_WINDOW
937
+ );
938
+ const windowBtc = sliceWindow(
939
+ btcReturns,
940
+ btcReturns.length - 1,
941
+ CANDLE_WINDOW
942
+ );
943
+ const windowRel = sliceWindow(
944
+ relReturns,
945
+ relReturns.length - 1,
946
+ CANDLE_WINDOW
947
+ );
948
+ row[altKey("Ret_Mean")] = computeMean(windowAlt);
949
+ row[altKey("Ret_Std")] = computeStd(windowAlt);
950
+ row[altKey("Ret_Skew")] = computeSkew(windowAlt);
951
+ row[altKey("Ret_Kurt")] = computeKurtosis(windowAlt);
952
+ row[btcKey("Ret_Mean")] = computeMean(windowBtc);
953
+ row[btcKey("Ret_Std")] = computeStd(windowBtc);
954
+ row[btcKey("Ret_Skew")] = computeSkew(windowBtc);
955
+ row[btcKey("Ret_Kurt")] = computeKurtosis(windowBtc);
956
+ row[key("RelRet_Mean")] = computeMean(windowRel);
957
+ row[key("RelRet_Std")] = computeStd(windowRel);
958
+ row[key("RelRet_Skew")] = computeSkew(windowRel);
959
+ row[key("RelRet_Kurt")] = computeKurtosis(windowRel);
960
+ };
961
+ var buildMlTrainingRow = (signalRecord, resultRecord) => {
962
+ const { signal, context } = signalRecord;
963
+ const indicators = dropLastFromIndicatorSeries(
964
+ signal?.indicators ?? {}
965
+ );
966
+ const currentPrice = toNumber(signal?.prices?.currentPrice, 0);
967
+ const candleList = normalizeCandles(indicators.candles15m);
968
+ const btcList = normalizeCandles(indicators.btcCandles15m);
969
+ const entryTimestamp = toNumber(signal?.timestamp, 0);
970
+ const intervalMinutes = toNumber(signal?.interval, 0);
971
+ const entryDate = entryTimestamp > 0 ? new Date(entryTimestamp) : null;
972
+ const entryHour = entryDate ? entryDate.getUTCHours() : 0;
973
+ const entryDayOfWeek = entryDate ? entryDate.getUTCDay() : 0;
974
+ const row = {
975
+ symbol: normalizeSymbol(
976
+ signal?.symbol ?? context?.symbol ?? resultRecord?.symbol ?? ""
977
+ ),
978
+ strategy: normalizeSymbol(signal?.strategy ?? context?.strategyName ?? ""),
979
+ direction: signal?.direction === "LONG" ? 1 : 0,
980
+ entryTimestamp,
981
+ takeProfitPrice: safeDiv2(
982
+ currentPrice,
983
+ toNumber(signal?.prices?.takeProfitPrice, 0)
984
+ ),
985
+ stopLossPrice: safeDiv2(
986
+ currentPrice,
987
+ toNumber(signal?.prices?.stopLossPrice, 0)
988
+ ),
989
+ riskRatio: toNumber(signal?.prices?.riskRatio, 0),
990
+ Correlation: toNumber(signal?.indicators?.correlation, 0),
991
+ Touches: toNumber(
992
+ signal?.additionalIndicators?.touches ?? signal?.indicators?.touches,
993
+ 0
994
+ ),
995
+ Distance: toNumber(
996
+ signal?.additionalIndicators?.distance ?? signal?.indicators?.distance,
997
+ 0
998
+ ),
999
+ Ctx_EntryHour: entryHour,
1000
+ Ctx_EntryDayOfWeek: entryDayOfWeek,
1001
+ Ctx_EntryHourSin: Math.sin(2 * Math.PI * entryHour / 24),
1002
+ Ctx_EntryHourCos: Math.cos(2 * Math.PI * entryHour / 24),
1003
+ Ctx_StopDistance: clamp2(
1004
+ 1 - safeDiv2(currentPrice, toNumber(signal?.prices?.stopLossPrice, 0)),
1005
+ -5,
1006
+ 5
1007
+ ),
1008
+ Ctx_TakeDistance: clamp2(
1009
+ safeDiv2(currentPrice, toNumber(signal?.prices?.takeProfitPrice, 0)) - 1,
1010
+ -5,
1011
+ 5
1012
+ )
1013
+ };
1014
+ row.Ctx_RiskAsymmetry = clamp2(
1015
+ safeDiv2(
1016
+ toNumber(row.Ctx_TakeDistance, 0),
1017
+ Math.abs(toNumber(row.Ctx_StopDistance, 0))
1018
+ ),
1019
+ -10,
1020
+ 10
1021
+ );
1022
+ const addSeries = (prefix, values, priceBase = currentPrice) => {
1023
+ const series = padSeries(normalizeSeries(values));
1024
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1025
+ const value = series[i];
1026
+ row[`${prefix}_${i + 1}`] = safeDiv2(value, priceBase);
1027
+ }
1028
+ };
1029
+ const addSeriesBackwardReturns = (prefix, values) => {
1030
+ const series = padSeries(normalizeSeries(values));
1031
+ const returns = backwardReturns(series);
1032
+ assignBackwardReturns(row, prefix, returns);
1033
+ };
1034
+ const addSeriesRaw = (prefix, values) => {
1035
+ const series = padSeries(normalizeSeries(values));
1036
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1037
+ row[`${prefix}_${i + 1}`] = series[i];
1038
+ }
1039
+ };
1040
+ const addSeriesPct = (prefix, values) => {
1041
+ const series = padSeries(normalizeSeries(values));
1042
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1043
+ row[`${prefix}_${i + 1}`] = squash(series[i], 10);
1044
+ }
1045
+ };
1046
+ const addSeriesMoments = (prefix, values) => {
1047
+ const series = padSeries(normalizeSeries(values));
1048
+ row[`${prefix}_Mean`] = computeMean(series);
1049
+ row[`${prefix}_Std`] = computeStd(series);
1050
+ row[`${prefix}_Skew`] = computeSkew(series);
1051
+ row[`${prefix}_Kurt`] = computeKurtosis(series);
1052
+ };
1053
+ const addSeriesStd = (prefix, values) => {
1054
+ const series = padSeries(normalizeSeries(values));
1055
+ const standardized = standardizeSeries(series);
1056
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1057
+ row[`${prefix}_${i + 1}`] = standardized[i] ?? 0;
1058
+ }
1059
+ };
1060
+ const addSeriesRelTo = (prefix, values, denomSeries) => {
1061
+ const series = padSeries(normalizeSeries(values));
1062
+ const denomSeriesSafe = padSeries(normalizeSeries(denomSeries));
1063
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1064
+ const value = series[i];
1065
+ const denom = denomSeriesSafe[i];
1066
+ row[`${prefix}_${i + 1}`] = safeDiv2(value, denom);
1067
+ }
1068
+ };
1069
+ const addSeriesLogVolume = (prefix, values) => {
1070
+ const series = padSeries(normalizeSeries(values));
1071
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1072
+ const value = series[i];
1073
+ row[`${prefix}_${i + 1}`] = clamp2(safeLog1p(value), -20, 20);
1074
+ }
1075
+ };
1076
+ const addSeriesLogVolumeBackwardReturns = (prefix, values) => {
1077
+ const series = padSeries(normalizeSeries(values));
1078
+ const loggedSeries = series.map((value) => safeLog1p(value));
1079
+ const returns = backwardReturns(loggedSeries);
1080
+ assignBackwardReturns(row, prefix, returns);
1081
+ };
1082
+ const addSeriesVolumeMedianNormalized = (prefix, values) => {
1083
+ const numericValues = padSeries(normalizeSeries(values));
1084
+ const windowSize = Math.min(20, numericValues.length);
1085
+ for (let i = 1; i < INDICATOR_WINDOW; i += 1) {
1086
+ const value = numericValues[i];
1087
+ const window = sliceWindow(numericValues, i, windowSize);
1088
+ const median2 = computeMedian(window);
1089
+ row[`${prefix}_${i + 1}_MedianNorm`] = clamp2(
1090
+ safeDiv2(value, median2),
1091
+ 0,
1092
+ 20
1093
+ );
1094
+ }
1095
+ };
1096
+ const keyWithSourceSuffix = (key, sourceSuffix, sourcePrefix) => {
1097
+ const baseKey = sourcePrefix ? `${sourcePrefix}${key[0].toUpperCase()}${key.slice(1)}` : key;
1098
+ return sourceSuffix ? `${baseKey}${sourceSuffix}` : baseKey;
1099
+ };
1100
+ const keyWithFeaturePrefix = (key, featurePrefix) => featurePrefix ? `${featurePrefix}_${key}` : key;
1101
+ const addIndicatorFeatures = (featurePrefix, sourceSuffix, sourcePrefix, priceBase, assetPrefix = "ALT") => {
1102
+ const indicatorSeries = (key) => asArray(indicators[keyWithSourceSuffix(key, sourceSuffix, sourcePrefix)]);
1103
+ const featureKey = (key) => keyWithFeaturePrefix(`${assetPrefix}_${key}`, featurePrefix);
1104
+ const backwardReturnSeries = [
1105
+ ["MA_Fast", "maFast"],
1106
+ ["MA_Medium", "maMedium"],
1107
+ ["MA_Slow", "maSlow"],
1108
+ ["BB_Upper", "bbUpper"],
1109
+ ["BB_Middle", "bbMiddle"],
1110
+ ["BB_Lower", "bbLower"]
1111
+ ];
1112
+ const logVolumeBackwardReturnSeries = [
1113
+ ["OBV_LogRet", "obv"],
1114
+ ["SMA_OBV_LogRet", "smaObv"]
1115
+ ];
1116
+ const momentsSeries = [
1117
+ ["ATR_PCT", "atrPct"],
1118
+ ["BB_Upper", "bbUpper"],
1119
+ ["BB_Middle", "bbMiddle"],
1120
+ ["BB_Lower", "bbLower"],
1121
+ ["MACD_Histogram", "macdHistogram"],
1122
+ ["Price24hPcnt", "price24hPcnt"],
1123
+ ["Price1hPcnt", "price1hPcnt"]
1124
+ ];
1125
+ const stdSeries = [
1126
+ ["MACD", "macd"],
1127
+ ["MACD_Signal", "macdSignal"],
1128
+ ["MACD_Histogram", "macdHistogram"]
1129
+ ];
1130
+ const pctSeries = [
1131
+ ["Price24hPcnt", "price24hPcnt"],
1132
+ ["Price1hPcnt", "price1hPcnt"]
1133
+ ];
1134
+ const volumeSeries = [
1135
+ ["Volume1h", "volume1h"],
1136
+ ["Volume24h", "volume24h"]
1137
+ ];
1138
+ const relToSeries = [
1139
+ ["HighPrice1h", "highPrice1h", "maMedium"],
1140
+ ["LowPrice1h", "lowPrice1h", "maMedium"],
1141
+ ["HighPrice24h", "highPrice24h", "maMedium"],
1142
+ ["LowPrice24h", "lowPrice24h", "maMedium"],
1143
+ ["HighLevel", "highLevel", "maMedium"],
1144
+ ["LowLevel", "lowLevel", "maMedium"],
1145
+ ["PrevClose", "prevClose", "maMedium"]
1146
+ ];
1147
+ addSeries(featureKey("ATR"), indicatorSeries("atr"), priceBase);
1148
+ for (const [featureName, sourceName] of backwardReturnSeries) {
1149
+ addSeriesBackwardReturns(
1150
+ featureKey(featureName),
1151
+ indicatorSeries(sourceName)
1152
+ );
1153
+ }
1154
+ for (const [featureName, sourceName] of logVolumeBackwardReturnSeries) {
1155
+ addSeriesLogVolumeBackwardReturns(
1156
+ featureKey(featureName),
1157
+ indicatorSeries(sourceName)
1158
+ );
1159
+ }
1160
+ addSeriesRaw(featureKey("ATR_PCT"), indicatorSeries("atrPct"));
1161
+ if (!sourcePrefix && !sourceSuffix && assetPrefix === "ALT") {
1162
+ addSeriesRaw(featureKey("Spread"), indicatorSeries("spread"));
1163
+ }
1164
+ for (const [featureName, sourceName] of momentsSeries) {
1165
+ addSeriesMoments(featureKey(featureName), indicatorSeries(sourceName));
1166
+ }
1167
+ for (const [featureName, sourceName] of stdSeries) {
1168
+ addSeriesStd(featureKey(featureName), indicatorSeries(sourceName));
1169
+ }
1170
+ for (const [featureName, sourceName] of pctSeries) {
1171
+ addSeriesPct(featureKey(featureName), indicatorSeries(sourceName));
1172
+ }
1173
+ for (const [featureName, sourceName, denomName] of relToSeries) {
1174
+ addSeriesRelTo(
1175
+ featureKey(featureName),
1176
+ indicatorSeries(sourceName),
1177
+ indicatorSeries(denomName)
1178
+ );
1179
+ }
1180
+ for (const [featureName, sourceName] of volumeSeries) {
1181
+ const series = indicatorSeries(sourceName);
1182
+ addSeriesLogVolume(featureKey(featureName), series);
1183
+ addSeriesVolumeMedianNormalized(featureKey(featureName), series);
1184
+ }
1185
+ };
1186
+ const applyIndicatorAndCandlePhases = () => {
1187
+ const maMediumSeries = asArray(indicators.maMedium);
1188
+ const btcCandlesByTimeframe = {
1189
+ TF15M: btcList,
1190
+ TF1H: normalizeCandles(indicators.btcCandles1h),
1191
+ TF4H: normalizeCandles(indicators.btcCandles4h),
1192
+ TF1D: normalizeCandles(indicators.btcCandles1d)
1193
+ };
1194
+ const btcPriceByTimeframe = {
1195
+ TF15M: toNumber(
1196
+ btcCandlesByTimeframe.TF15M[btcCandlesByTimeframe.TF15M.length - 1]?.close,
1197
+ currentPrice
1198
+ ),
1199
+ TF1H: toNumber(
1200
+ btcCandlesByTimeframe.TF1H[btcCandlesByTimeframe.TF1H.length - 1]?.close,
1201
+ currentPrice
1202
+ ),
1203
+ TF4H: toNumber(
1204
+ btcCandlesByTimeframe.TF4H[btcCandlesByTimeframe.TF4H.length - 1]?.close,
1205
+ currentPrice
1206
+ ),
1207
+ TF1D: toNumber(
1208
+ btcCandlesByTimeframe.TF1D[btcCandlesByTimeframe.TF1D.length - 1]?.close,
1209
+ currentPrice
1210
+ )
1211
+ };
1212
+ for (const timeframe of INDICATOR_TIMEFRAMES) {
1213
+ addIndicatorFeatures(
1214
+ timeframe.label,
1215
+ timeframe.suffix,
1216
+ void 0,
1217
+ void 0,
1218
+ "ALT"
1219
+ );
1220
+ addIndicatorFeatures(
1221
+ timeframe.label,
1222
+ timeframe.suffix,
1223
+ "btc",
1224
+ btcPriceByTimeframe[timeframe.label],
1225
+ "BTC"
1226
+ );
1227
+ }
1228
+ const basePriceScale = padSeries(normalizeSeries(maMediumSeries));
1229
+ for (const timeframe of CANDLE_TIMEFRAMES) {
1230
+ const tfCandlesFromIndicators = timeframe.key === "candles15m" ? candleList : normalizeCandles(indicators[timeframe.key]);
1231
+ const tfBtcCandlesFromIndicators = timeframe.btcKey === "btcCandles15m" ? btcList : normalizeCandles(indicators[timeframe.btcKey]);
1232
+ const tfCandles = tfCandlesFromIndicators.slice(-CANDLE_WINDOW);
1233
+ const tfBtcCandles = tfBtcCandlesFromIndicators.slice(-CANDLE_WINDOW);
1234
+ const priceScaleSeries = timeframe.label === "TF15M" ? basePriceScale : tfCandles.map((candle) => candle.close);
1235
+ addCandleFeatures(row, {
1236
+ featurePrefix: timeframe.label,
1237
+ candles: tfCandles,
1238
+ btcCandles: tfBtcCandles,
1239
+ currentPrice,
1240
+ priceScaleSeries
1241
+ });
1242
+ }
1243
+ };
1244
+ const applySeriesAnalysisPhase = () => {
1245
+ const tfConfigByLabel = {
1246
+ TF15M: {
1247
+ indicatorSuffix: "",
1248
+ candleKey: "candles15m",
1249
+ btcCandleKey: "btcCandles15m"
1250
+ },
1251
+ TF1H: {
1252
+ indicatorSuffix: "1h",
1253
+ candleKey: "candles1h",
1254
+ btcCandleKey: "btcCandles1h"
1255
+ },
1256
+ TF4H: {
1257
+ indicatorSuffix: "4h",
1258
+ candleKey: "candles4h",
1259
+ btcCandleKey: "btcCandles4h"
1260
+ },
1261
+ TF1D: {
1262
+ indicatorSuffix: "1d",
1263
+ candleKey: "candles1d",
1264
+ btcCandleKey: "btcCandles1d"
1265
+ }
1266
+ };
1267
+ const keyWithSuffix = (baseKey, suffix) => suffix ? `${baseKey}${suffix}` : baseKey;
1268
+ const keyWithBtcPrefix = (baseKey, suffix) => {
1269
+ const withSuffix = keyWithSuffix(baseKey, suffix);
1270
+ return `btc${withSuffix[0].toUpperCase()}${withSuffix.slice(1)}`;
1271
+ };
1272
+ const altSummaries = {};
1273
+ for (const tf of INDICATOR_TIMEFRAMES) {
1274
+ const cfg = tfConfigByLabel[tf.label];
1275
+ const altCandles = cfg.candleKey === "candles15m" ? candleList : normalizeCandles(indicators[cfg.candleKey]);
1276
+ const btcCandles = cfg.btcCandleKey === "btcCandles15m" ? btcList : normalizeCandles(indicators[cfg.btcCandleKey]);
1277
+ const altSummary = analyzeMlSeriesWindow({
1278
+ candles: altCandles.slice(-CANDLE_WINDOW),
1279
+ benchmarkCandles: btcCandles.slice(-CANDLE_WINDOW),
1280
+ indicators: {
1281
+ atrPct: normalizeSeries(
1282
+ indicators[keyWithSuffix("atrPct", cfg.indicatorSuffix)]
1283
+ ),
1284
+ price1hPcnt: normalizeSeries(
1285
+ indicators[keyWithSuffix("price1hPcnt", cfg.indicatorSuffix)]
1286
+ ),
1287
+ price24hPcnt: normalizeSeries(
1288
+ indicators[keyWithSuffix("price24hPcnt", cfg.indicatorSuffix)]
1289
+ ),
1290
+ macdHistogram: normalizeSeries(
1291
+ indicators[keyWithSuffix("macdHistogram", cfg.indicatorSuffix)]
1292
+ ),
1293
+ maFast: normalizeSeries(
1294
+ indicators[keyWithSuffix("maFast", cfg.indicatorSuffix)]
1295
+ ),
1296
+ maSlow: normalizeSeries(
1297
+ indicators[keyWithSuffix("maSlow", cfg.indicatorSuffix)]
1298
+ )
1299
+ }
1300
+ });
1301
+ altSummaries[tf.label] = altSummary;
1302
+ for (const [featureName, value] of Object.entries(altSummary)) {
1303
+ row[`${tf.label}_ALT_ANALYSIS_${featureName}`] = value;
1304
+ }
1305
+ const btcSummary = analyzeMlSeriesWindow({
1306
+ candles: btcCandles.slice(-CANDLE_WINDOW),
1307
+ indicators: {
1308
+ atrPct: normalizeSeries(
1309
+ indicators[keyWithBtcPrefix("atrPct", cfg.indicatorSuffix)]
1310
+ ),
1311
+ price1hPcnt: normalizeSeries(
1312
+ indicators[keyWithBtcPrefix("price1hPcnt", cfg.indicatorSuffix)]
1313
+ ),
1314
+ price24hPcnt: normalizeSeries(
1315
+ indicators[keyWithBtcPrefix("price24hPcnt", cfg.indicatorSuffix)]
1316
+ ),
1317
+ macdHistogram: normalizeSeries(
1318
+ indicators[keyWithBtcPrefix("macdHistogram", cfg.indicatorSuffix)]
1319
+ ),
1320
+ maFast: normalizeSeries(
1321
+ indicators[keyWithBtcPrefix("maFast", cfg.indicatorSuffix)]
1322
+ ),
1323
+ maSlow: normalizeSeries(
1324
+ indicators[keyWithBtcPrefix("maSlow", cfg.indicatorSuffix)]
1325
+ )
1326
+ }
1327
+ });
1328
+ for (const [featureName, value] of Object.entries(btcSummary)) {
1329
+ row[`${tf.label}_BTC_ANALYSIS_${featureName}`] = value;
1330
+ }
1331
+ }
1332
+ const alignmentPairs = [
1333
+ ["TF15M", "TF1H"],
1334
+ ["TF1H", "TF4H"],
1335
+ ["TF4H", "TF1D"],
1336
+ ["TF15M", "TF4H"]
1337
+ ];
1338
+ for (const [leftTf, rightTf] of alignmentPairs) {
1339
+ const alignment = buildMlSeriesAlignment(
1340
+ altSummaries[leftTf],
1341
+ altSummaries[rightTf]
1342
+ );
1343
+ for (const [featureName, value] of Object.entries(alignment)) {
1344
+ row[`MTF_ALT_${leftTf}_${rightTf}_ANALYSIS_${featureName}`] = value;
1345
+ }
1346
+ }
1347
+ };
1348
+ const applyRegimePhase = () => {
1349
+ const atrPctSeries = padSeries(normalizeSeries(indicators.atrPct));
1350
+ const price1hPctSeries = padSeries(normalizeSeries(indicators.price1hPcnt));
1351
+ const tf15mReturns = backwardReturns(
1352
+ candleList.slice(-INDICATOR_WINDOW).map((candle) => toNumber(candle.close, 0))
1353
+ );
1354
+ const atrPctLast = atrPctSeries[atrPctSeries.length - 1] ?? 0;
1355
+ const atrPctMean = computeMean(atrPctSeries);
1356
+ const atrPctStd = computeStd(atrPctSeries);
1357
+ const atrPctZ = atrPctStd > 0 ? (atrPctLast - atrPctMean) / atrPctStd : 0;
1358
+ const atrPctRank = percentileRank2(atrPctSeries, atrPctLast);
1359
+ const realizedVol = computeStd(tf15mReturns);
1360
+ const realizedVolRank = percentileRank2(
1361
+ tf15mReturns.length ? tf15mReturns : [0],
1362
+ tf15mReturns.length ? tf15mReturns[tf15mReturns.length - 1] : 0
1363
+ );
1364
+ const trendStrength = Math.abs(computeMean(price1hPctSeries));
1365
+ row.Regime_ATR_PCT_Last = atrPctLast;
1366
+ row.Regime_ATR_PCT_Z = clamp2(atrPctZ, -8, 8);
1367
+ row.Regime_ATR_PCT_Rank = atrPctRank;
1368
+ row.Regime_RealizedVol = realizedVol;
1369
+ row.Regime_RealizedVol_Rank = realizedVolRank;
1370
+ row.Regime_TrendStrength = trendStrength;
1371
+ row.Regime_IsHighVol = atrPctRank >= 0.7 || realizedVolRank >= 0.7 ? 1 : 0;
1372
+ const latestIdx = INDICATOR_WINDOW;
1373
+ row.Ctx_DistanceTo24hRange = clamp2(
1374
+ toNumber(row[`TF15M_ALT_HighPrice24h_${latestIdx}`], 0) - toNumber(row[`TF15M_ALT_LowPrice24h_${latestIdx}`], 0),
1375
+ -10,
1376
+ 10
1377
+ );
1378
+ };
1379
+ const applyTrendlinePhase = () => {
1380
+ const trendLine = signal?.figures?.trendLine ?? signal?.additionalIndicators?.trendLine ?? {};
1381
+ row.TrendLine_Mode = trendLine?.mode === "highs" ? 1 : 0;
1382
+ row.TrendLine_Distance = toNumber(trendLine?.distance, 0);
1383
+ const trendAlpha = padSeries(normalizeSeries(trendLine?.alpha));
1384
+ for (let i = 0; i < INDICATOR_WINDOW; i += 1) {
1385
+ row[`TrendLine_Alpha_${i + 1}`] = trendAlpha[i];
1386
+ }
1387
+ const points = asArray(
1388
+ trendLine?.points
1389
+ );
1390
+ const maxPoints = 2;
1391
+ for (let i = 0; i < maxPoints; i += 1) {
1392
+ const point = points[i] ?? {};
1393
+ row[`POINTS_VALUE_${i + 1}`] = safeDiv2(
1394
+ toNumber(point?.value, 0),
1395
+ currentPrice
1396
+ );
1397
+ const pointDeltaMs = entryTimestamp - toNumber(point?.timestamp, 0);
1398
+ const pointDeltaMin = safeDiv2(pointDeltaMs, 6e4);
1399
+ if (i === 0) {
1400
+ const pointDeltaBars = intervalMinutes > 0 ? safeDiv2(pointDeltaMin, intervalMinutes) : pointDeltaMin;
1401
+ row[`POINTS_TS_${i + 1}`] = safeLog1pPositive(pointDeltaBars);
1402
+ }
1403
+ }
1404
+ const touches = asArray(
1405
+ trendLine?.touches
1406
+ ).map((touch) => ({
1407
+ value: toNumber(touch?.value, NaN),
1408
+ timestamp: toNumber(touch?.timestamp, NaN)
1409
+ })).filter(
1410
+ (touch) => Number.isFinite(touch.value) && Number.isFinite(touch.timestamp)
1411
+ ).sort((a, b) => a.timestamp - b.timestamp).slice(-3);
1412
+ const maxTouches = 3;
1413
+ for (let i = 0; i < maxTouches; i += 1) {
1414
+ const touch = touches[i];
1415
+ if (!touch) {
1416
+ row[`TOUCHES_VALUE_${i + 1}`] = 0;
1417
+ row[`TOUCHES_TS_${i + 1}`] = 0;
1418
+ continue;
1419
+ }
1420
+ row[`TOUCHES_VALUE_${i + 1}`] = safeDiv2(
1421
+ toNumber(touch?.value, 0),
1422
+ currentPrice
1423
+ );
1424
+ const touchDeltaMs = entryTimestamp - toNumber(touch?.timestamp, 0);
1425
+ const touchDeltaMin = safeDiv2(touchDeltaMs, 6e4);
1426
+ const touchDeltaBars = intervalMinutes > 0 ? safeDiv2(touchDeltaMin, intervalMinutes) : touchDeltaMin;
1427
+ row[`TOUCHES_TS_${i + 1}`] = safeLog1pPositive(touchDeltaBars);
1428
+ }
1429
+ const normalizedPoints = points.map((point) => ({
1430
+ value: toNumber(point?.value, NaN),
1431
+ timestamp: toNumber(point?.timestamp, NaN)
1432
+ })).filter(
1433
+ (point) => Number.isFinite(point.value) && Number.isFinite(point.timestamp)
1434
+ ).sort((a, b) => a.timestamp - b.timestamp);
1435
+ if (normalizedPoints.length >= 2) {
1436
+ const p1 = normalizedPoints[normalizedPoints.length - 2];
1437
+ const p2 = normalizedPoints[normalizedPoints.length - 1];
1438
+ const dtMs = p2.timestamp - p1.timestamp;
1439
+ if (dtMs !== 0) {
1440
+ const slopePerMs = (p2.value - p1.value) / dtMs;
1441
+ const tlAtEntry = p1.value + slopePerMs * (entryTimestamp - p1.timestamp);
1442
+ const slopePerBar = intervalMinutes > 0 ? slopePerMs * intervalMinutes * 6e4 : null;
1443
+ row.TrendLine_Delta_To_Price = safeDiv2(
1444
+ currentPrice - tlAtEntry,
1445
+ currentPrice
1446
+ );
1447
+ row.TrendLine_Slope = slopePerBar == null ? null : safeLog1p(slopePerBar);
1448
+ } else {
1449
+ row.TrendLine_Delta_To_Price = null;
1450
+ row.TrendLine_Slope = null;
1451
+ }
1452
+ } else {
1453
+ row.TrendLine_Delta_To_Price = null;
1454
+ row.TrendLine_Slope = null;
1455
+ }
1456
+ };
1457
+ const applyLabelPhase = () => {
1458
+ const profit = toNumber(resultRecord?.profit, NaN);
1459
+ row.label = Number.isFinite(profit) ? profit > 0 ? 1 : 0 : null;
1460
+ row.profit = Number.isFinite(profit) ? profit : null;
1461
+ };
1462
+ applyIndicatorAndCandlePhases();
1463
+ applyRegimePhase();
1464
+ applySeriesAnalysisPhase();
1465
+ applyTrendlinePhase();
1466
+ applyLabelPhase();
1467
+ return row;
1468
+ };
1469
+ var trimMlTrainingRowWindows = (row, keep = ML_WINDOW_POLICY.outputWindow) => {
1470
+ const indexedKeyPattern = /^(.*_)(\d+)(?:(_.*))?$/;
1471
+ const groupMaxIndex = /* @__PURE__ */ new Map();
1472
+ const groupKeyCount = /* @__PURE__ */ new Map();
1473
+ for (const key of Object.keys(row)) {
1474
+ const match = key.match(indexedKeyPattern);
1475
+ if (!match) continue;
1476
+ const index = Number(match[2]);
1477
+ if (!Number.isFinite(index)) continue;
1478
+ const signature = `${match[1]}|${match[3] ?? ""}`;
1479
+ const prev = groupMaxIndex.get(signature) ?? 0;
1480
+ if (index > prev) {
1481
+ groupMaxIndex.set(signature, index);
1482
+ }
1483
+ groupKeyCount.set(signature, (groupKeyCount.get(signature) ?? 0) + 1);
1484
+ }
1485
+ const next = {};
1486
+ for (const [key, value] of Object.entries(row)) {
1487
+ const match = key.match(indexedKeyPattern);
1488
+ if (!match) {
1489
+ next[key] = value;
1490
+ continue;
1491
+ }
1492
+ const prefix = match[1];
1493
+ const suffix = match[3] ?? "";
1494
+ const signature = `${prefix}|${suffix}`;
1495
+ const maxIndex = groupMaxIndex.get(signature) ?? 0;
1496
+ const keyCount = groupKeyCount.get(signature) ?? 0;
1497
+ if (maxIndex <= keep || keyCount <= keep) {
1498
+ next[key] = value;
1499
+ continue;
1500
+ }
1501
+ const index = Number(match[2]);
1502
+ const firstKeptIndex = maxIndex - keep + 1;
1503
+ if (index < firstKeptIndex || index > maxIndex) {
1504
+ continue;
1505
+ }
1506
+ const newIndex = index - firstKeptIndex + 1;
1507
+ next[`${prefix}${newIndex}${suffix}`] = value;
1508
+ }
1509
+ return next;
1510
+ };
1511
+
1512
+ // src/mlWindowing.ts
1513
+ var DAY_MS = 24 * 60 * 60 * 1e3;
1514
+ var toIsoUtcOrNull = (value) => {
1515
+ if (!Number.isFinite(value) || !value || value <= 0) return null;
1516
+ return new Date(Number(value)).toISOString();
1517
+ };
1518
+ var isDerivedDatasetFileName = (name) => [
1519
+ ".holdout-train.",
1520
+ ".holdout-test.",
1521
+ ".walk-forward.",
1522
+ ".walk-forward-",
1523
+ ".prod."
1524
+ ].some((token) => name.includes(token));
1525
+ var computeWindowBoundaries = ({
1526
+ maxLabeledTs,
1527
+ maxTrainTs,
1528
+ testDays,
1529
+ trainRecentDays,
1530
+ walkForwardFolds
1531
+ }) => {
1532
+ const holdoutCutoffMs = maxLabeledTs - testDays * DAY_MS;
1533
+ const holdoutTrainStartMs = trainRecentDays > 0 ? maxTrainTs - trainRecentDays * DAY_MS : Number.NEGATIVE_INFINITY;
1534
+ const wfStartMs = trainRecentDays > 0 ? maxTrainTs - (trainRecentDays + Math.max(walkForwardFolds, 0) * testDays) * DAY_MS : Number.NEGATIVE_INFINITY;
1535
+ const prodStartMs = trainRecentDays > 0 ? maxLabeledTs - trainRecentDays * DAY_MS : Number.NEGATIVE_INFINITY;
1536
+ const folds = Array.from(
1537
+ { length: Math.max(walkForwardFolds, 0) },
1538
+ (_, i) => {
1539
+ const fold = i + 1;
1540
+ const endTs = maxTrainTs - (fold - 1) * testDays * DAY_MS;
1541
+ const startTs = endTs - testDays * DAY_MS;
1542
+ return { fold, startTs, endTs };
1543
+ }
1544
+ );
1545
+ return {
1546
+ holdoutCutoffMs,
1547
+ holdoutTrainStartMs,
1548
+ wfStartMs,
1549
+ prodStartMs,
1550
+ folds
1551
+ };
1552
+ };
1553
+
1554
+ // src/mlCausalityGuard.ts
1555
+ var parseTimestampMs = (value) => {
1556
+ const numeric = Number(value);
1557
+ if (!Number.isFinite(numeric) || numeric <= 0) return null;
1558
+ return numeric < 1e12 ? Math.trunc(numeric * 1e3) : Math.trunc(numeric);
1559
+ };
1560
+ var LOOKAHEAD_TS_KEY_RE = /(Timestamp|Ts|AtMs)$/;
1561
+ var isTimestampFeatureKey = (key) => {
1562
+ if (!key || key === "entryTimestamp") return false;
1563
+ return LOOKAHEAD_TS_KEY_RE.test(key);
1564
+ };
1565
+ var findLookaheadViolations = (row) => {
1566
+ const entryTimestampMs = parseTimestampMs(row.entryTimestamp);
1567
+ if (!entryTimestampMs) return [];
1568
+ const violations = [];
1569
+ for (const [key, value] of Object.entries(row)) {
1570
+ if (!isTimestampFeatureKey(key)) continue;
1571
+ const featureTimestampMs = parseTimestampMs(value);
1572
+ if (!featureTimestampMs) continue;
1573
+ if (featureTimestampMs > entryTimestampMs) {
1574
+ violations.push({
1575
+ key,
1576
+ entryTimestampMs,
1577
+ featureTimestampMs
1578
+ });
1579
+ }
1580
+ }
1581
+ return violations;
1582
+ };
1583
+ // Annotate the CommonJS export names for ESM import in node:
1584
+ 0 && (module.exports = {
1585
+ analyzeMlSeriesWindow,
1586
+ appendMlDatasetRow,
1587
+ buildMlFeatures,
1588
+ buildMlSeriesAlignment,
1589
+ buildMlTrainingRow,
1590
+ closeAllMlDatasetWriters,
1591
+ closeMlDatasetWriter,
1592
+ computeWindowBoundaries,
1593
+ fetchMlThreshold,
1594
+ findLookaheadViolations,
1595
+ flushMlDatasetWriter,
1596
+ getMlChunkFilePath,
1597
+ isDerivedDatasetFileName,
1598
+ isTimestampFeatureKey,
1599
+ listMlChunkFiles,
1600
+ mergeJsonlFiles,
1601
+ toFileToken,
1602
+ toIsoUtcOrNull,
1603
+ trimMlTrainingRowWindows
1604
+ });