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