neuroalpha 0.2.0 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +7 -3
- package/bin/cli.js +69 -69
- package/config/seed-run.json +1 -0
- package/config/strategy.json +4 -10
- package/deploy/install.sh +128 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +103 -0
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -1
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +171 -0
- package/package.json +15 -5
- package/public/admin.html +426 -0
- package/public/app.js +741 -251
- package/public/index.html +45 -20
- package/public/landing.html +1004 -870
- package/public/legal.html +22 -0
- package/public/styles.css +49 -15
- package/server/api/routes.js +176 -23
- package/server/auth/index.js +544 -340
- package/server/billing/yookassa.js +1 -0
- package/server/brokers/index.js +1224 -645
- package/server/config.js +6 -4
- package/server/data/moex.js +1 -0
- package/server/db.js +53 -2
- package/server/hub.js +1 -0
- package/server/index.js +10 -2
- package/server/llm/client.js +1 -0
- package/server/llm/providers.js +50 -3
- package/server/model/dataset.js +362 -352
- package/server/model/forecast.js +15 -21
- package/server/model/linalg.js +1 -0
- package/server/model/live.js +489 -486
- package/server/model/metrics.js +1 -0
- package/server/model/models.js +31 -21
- package/server/model/profiles.js +1 -0
- package/server/model/research.js +119 -172
- package/server/model/vocab.js +1 -0
- package/server/news/index.js +1 -0
- package/server/notify/index.js +304 -168
- package/server/params-meta.js +20 -25
- package/server/settings.js +1 -0
- package/server/strategist/analytics.js +1 -0
- package/server/strategist/index.js +6 -6
- package/server/strategist/tools.js +2 -1
- package/server/util.js +1 -0
- package/server/model/gpu-worker.html +0 -11
- package/server/model/gpu-worker.js +0 -60
- package/server/model/tf-node.js +0 -26
- package/server/model/transformer-core.js +0 -330
- package/server/model/transformer-gpu.js +0 -221
- package/server/model/transformer-pool.js +0 -200
- package/server/model/transformer-worker.js +0 -28
- package/server/model/transformer.js +0 -57
package/server/model/dataset.js
CHANGED
|
@@ -1,352 +1,362 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
c.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{ id: '
|
|
116
|
-
{ id: '
|
|
117
|
-
{ id: '
|
|
118
|
-
{ id: '
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
m.set(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
if (best
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
while (j < c.n && dayKey(c.ts[j])
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
divStat.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
j
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
2
|
+
import { ensureCandles, ensureDividends, exDate, mskToTs, tsToDate } from '../data/moex.js';
|
|
3
|
+
import { ensureVocab } from './vocab.js';
|
|
4
|
+
import { kvGet, kvSet } from '../db.js';
|
|
5
|
+
|
|
6
|
+
const MSK = 3 * 3600000;
|
|
7
|
+
const DAY = 86400000;
|
|
8
|
+
const dow = (ts) => (Math.floor((ts + MSK) / DAY) + 4) % 7;
|
|
9
|
+
const dayKey = (ts) => Math.floor((ts + MSK) / DAY);
|
|
10
|
+
const dayKeyToStr = (dk) => new Date(dk * DAY).toISOString().slice(0, 10);
|
|
11
|
+
|
|
12
|
+
// Кэш последних датасетов в памяти (не больше двух: исследование + живая проверка/прогноз).
|
|
13
|
+
// Вызывающий код датасет не мутирует — кэширование безопасно.
|
|
14
|
+
const datasetCache = new Map();
|
|
15
|
+
|
|
16
|
+
// Свечи, на которых работает модель: 10 минут (горизонт прогноза задаётся с точностью до 10 минут)
|
|
17
|
+
export const BAR_INTERVAL = 10;
|
|
18
|
+
export const BAR_MS = BAR_INTERVAL * 60000;
|
|
19
|
+
// горизонт в часах → число свечей (минимум одна свеча)
|
|
20
|
+
export const barsFromHours = (h) => Math.max(1, Math.round(Number(h) * (60 / BAR_INTERVAL)));
|
|
21
|
+
|
|
22
|
+
function cacheKey(interval, fromTs, tillTs, useMacro, useDividends) {
|
|
23
|
+
const day = Math.floor(fromTs / DAY);
|
|
24
|
+
const hour = Math.floor(tillTs / 3600000);
|
|
25
|
+
return JSON.stringify([interval, day, hour, useMacro ? 1 : 0, useDividends ? (process.env.DIVIDENDS_MODE || 1) : 0]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function pool(items, limit, fn) {
|
|
29
|
+
let i = 0;
|
|
30
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
31
|
+
while (i < items.length) { const k = i++; await fn(items[k], k); }
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Компактное хранение свечей: типизированные массивы вместо миллионов объектов (в ~15 раз меньше памяти)
|
|
36
|
+
function compact(rows) {
|
|
37
|
+
const n = rows.length;
|
|
38
|
+
const ts = new Float64Array(n), open = new Float64Array(n), high = new Float64Array(n), low = new Float64Array(n), close = new Float64Array(n), value = new Float64Array(n);
|
|
39
|
+
for (let i = 0; i < n; i++) {
|
|
40
|
+
const r = rows[i];
|
|
41
|
+
ts[i] = r.ts;
|
|
42
|
+
open[i] = r.open;
|
|
43
|
+
high[i] = r.high || r.close;
|
|
44
|
+
low[i] = r.low || r.close;
|
|
45
|
+
close[i] = r.close;
|
|
46
|
+
value[i] = r.value || 0;
|
|
47
|
+
}
|
|
48
|
+
return { n, ts, open, high, low, close, value };
|
|
49
|
+
}
|
|
50
|
+
const EMPTY = () => ({ n: 0, ts: new Float64Array(0), open: new Float64Array(0), high: new Float64Array(0), low: new Float64Array(0), close: new Float64Array(0), value: new Float64Array(0) });
|
|
51
|
+
|
|
52
|
+
// ---------- дивиденды: обратная корректировка цен (in-place по типизированным массивам) ----------
|
|
53
|
+
// Цены до экс-даты умножаем на (1 − дивиденд/закрытие предыдущей свечи), идя от поздних дивидендов к ранним.
|
|
54
|
+
// Тогда доходности, z-движения и результат сделок через отношение цен автоматически учитывают дивиденд.
|
|
55
|
+
// Источник дивидендов (сайт) бывает неточным: дубли, прогнозные строки, сдвиг даты. Поэтому дату отсечки сверяем с ценой:
|
|
56
|
+
// ночной разрыв (открытие дня к закрытию предыдущего) должен быть примерно равен дивиденду; ищем такой день рядом
|
|
57
|
+
// с расчётной датой (±2 дня), а если разрыва нет нигде — дивиденд не учитываем (иначе в ценах появился бы ложный скачок).
|
|
58
|
+
// verify=false — прежнее поведение (всегда по расчётной дате), только для сравнения.
|
|
59
|
+
const DIV_MIN = 0.005; // дивиденд меньше 0,5% от цены неотличим от шума — не корректируем
|
|
60
|
+
export function adjustDividends(c, divs, { verify = true } = {}) {
|
|
61
|
+
const stat = { applied: 0, skipped: 0 };
|
|
62
|
+
if (!c.n || !divs?.length) return stat;
|
|
63
|
+
const days = [], first = [];
|
|
64
|
+
let lastDk = -1;
|
|
65
|
+
for (let i = 0; i < c.n; i++) {
|
|
66
|
+
const dk = dayKey(c.ts[i]);
|
|
67
|
+
if (dk !== lastDk) { lastDk = dk; days.push(dayKeyToStr(dk)); first.push(i); }
|
|
68
|
+
}
|
|
69
|
+
const prevClose = (k) => { for (let j = first[k] - 1; j >= 0; j--) if (c.close[j] > 0) return c.close[j]; return NaN; };
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const sorted = divs
|
|
72
|
+
.filter((d) => { const key = `${d.registry}|${d.value}`; if (seen.has(key)) return false; seen.add(key); return true; }) // дубли источника
|
|
73
|
+
.sort((a, b) => (a.registry < b.registry ? 1 : -1)); // от поздних к ранним
|
|
74
|
+
for (const div of sorted) {
|
|
75
|
+
const ex = exDate(div.registry, days);
|
|
76
|
+
const di = ex ? days.indexOf(ex) : -1;
|
|
77
|
+
if (di < 1) continue;
|
|
78
|
+
const pc = prevClose(di);
|
|
79
|
+
const y = div.value / pc;
|
|
80
|
+
if (!(y >= DIV_MIN && y < 0.5)) { if (y >= 0.5) stat.skipped++; continue; } // у y≥50% — мусор (валюта и т.п.)
|
|
81
|
+
let k = di;
|
|
82
|
+
if (verify) {
|
|
83
|
+
const expected = Math.log(1 - y), tol = Math.max(0.5 * y, 0.003);
|
|
84
|
+
// крупный дивиденд ищем в ±2 днях, мелкий — только в расчётный день
|
|
85
|
+
const cand = y >= 0.01 ? [0, -1, 1, -2, 2] : [0];
|
|
86
|
+
let best = null;
|
|
87
|
+
for (const s of cand) {
|
|
88
|
+
const kk = di + s;
|
|
89
|
+
if (kk < 1 || kk >= days.length) continue;
|
|
90
|
+
const p0 = prevClose(kk), o = c.open[first[kk]];
|
|
91
|
+
if (!(p0 > 0 && o > 0)) continue;
|
|
92
|
+
const err = Math.abs(Math.log(o / p0) - expected);
|
|
93
|
+
if (err <= tol && (!best || err < best.err - 1e-9)) best = { kk, err };
|
|
94
|
+
}
|
|
95
|
+
if (!best) { stat.skipped++; continue; }
|
|
96
|
+
k = best.kk;
|
|
97
|
+
}
|
|
98
|
+
const i0 = first[k];
|
|
99
|
+
const base = prevClose(k);
|
|
100
|
+
const f = 1 - div.value / base;
|
|
101
|
+
if (!(f > 0.5 && f < 1.2)) { stat.skipped++; continue; }
|
|
102
|
+
const hasHL = c.high?.length === c.n && c.low?.length === c.n;
|
|
103
|
+
for (let j = 0; j < i0; j++) {
|
|
104
|
+
c.open[j] *= f;
|
|
105
|
+
c.close[j] *= f;
|
|
106
|
+
if (hasHL) { c.high[j] *= f; c.low[j] *= f; }
|
|
107
|
+
}
|
|
108
|
+
stat.applied++;
|
|
109
|
+
}
|
|
110
|
+
return stat;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------- макро-инструменты («рыночные токены») ----------
|
|
114
|
+
const MACRO = [
|
|
115
|
+
{ id: 'RGBI', label: 'облигации (RGBI)' },
|
|
116
|
+
{ id: 'CNYRUB_TOM', label: 'юань' },
|
|
117
|
+
{ id: 'USD000UTSTOM', label: 'доллар' },
|
|
118
|
+
{ id: 'GLDRUBF', label: 'золото' },
|
|
119
|
+
{ id: 'BR', label: 'нефть Brent' }, // склейка фьючерсов, см. loadBrent
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const MCODES = ['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z']; // месяцы в кодах фьючерсов
|
|
123
|
+
|
|
124
|
+
// Список существующих контрактов BR с их периодами (кэш на сутки, чтобы не пробовать все коды каждый раз)
|
|
125
|
+
async function brentContracts(fromTs, tillTs, signal) {
|
|
126
|
+
const cached = kvGet('brContracts');
|
|
127
|
+
let list = cached?.list;
|
|
128
|
+
if (!cached || Date.now() - cached.at > DAY) {
|
|
129
|
+
list = [];
|
|
130
|
+
const codes = [];
|
|
131
|
+
const y0 = new Date(fromTs - 40 * DAY).getUTCFullYear(), y1 = new Date(tillTs + 100 * DAY).getUTCFullYear();
|
|
132
|
+
for (let y = y0; y <= y1; y++) for (const m of MCODES) codes.push(`BR${m}${y % 10}`);
|
|
133
|
+
await pool(codes, 6, async (code) => {
|
|
134
|
+
try {
|
|
135
|
+
const rows = await ensureCandles(code, 24, fromTs - 35 * DAY, tillTs + 35 * DAY, { signal });
|
|
136
|
+
if (rows.length) list.push({ code, first: rows[0].ts, last: rows[rows.length - 1].ts });
|
|
137
|
+
} catch { /* контракта нет */ }
|
|
138
|
+
});
|
|
139
|
+
list.sort((a, b) => a.first - b.first);
|
|
140
|
+
kvSet('brContracts', { at: Date.now(), list });
|
|
141
|
+
}
|
|
142
|
+
return list.filter((c) => c.last >= fromTs && c.first <= tillTs);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Непрерывный ряд цены нефти: в каждый день — контракт с наибольшим оборотом дня,
|
|
146
|
+
// на стыке контрактов цена продолжается без скачка (доходность стыка = 0)
|
|
147
|
+
async function loadBrent(fromTs, tillTs, signal) {
|
|
148
|
+
const list = await brentContracts(fromTs, tillTs, signal);
|
|
149
|
+
const raw = [];
|
|
150
|
+
await pool(list, 4, async (c) => {
|
|
151
|
+
try {
|
|
152
|
+
const rows = await ensureCandles(c.code, BAR_INTERVAL, fromTs, tillTs, { signal });
|
|
153
|
+
if (rows.length) raw.push(compact(rows));
|
|
154
|
+
} catch { /* пропускаем */ }
|
|
155
|
+
});
|
|
156
|
+
// оборот каждого контракта по дням
|
|
157
|
+
const dayVol = new Map(); // day -> Map(idx -> vol)
|
|
158
|
+
raw.forEach((c, ci) => {
|
|
159
|
+
for (let i = 0; i < c.n; i++) {
|
|
160
|
+
const d = dayKey(c.ts[i]);
|
|
161
|
+
let m = dayVol.get(d);
|
|
162
|
+
if (!m) dayVol.set(d, (m = new Map()));
|
|
163
|
+
m.set(ci, (m.get(ci) || 0) + (c.value[i] || 0));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const days = [...dayVol.keys()].sort((a, b) => a - b);
|
|
167
|
+
// бары контракта по дням: ci -> Map(day -> [индексы]) — идём по возрастанию, так что просто указатели
|
|
168
|
+
const ptr = raw.map(() => 0);
|
|
169
|
+
const ts = [], close = [];
|
|
170
|
+
let factor = 1, prevClose = NaN, active = -1;
|
|
171
|
+
for (const d of days) {
|
|
172
|
+
const m = dayVol.get(d);
|
|
173
|
+
let best = -1, bv = -1;
|
|
174
|
+
for (const [ci, v] of m) if (v > bv) { bv = v; best = ci; }
|
|
175
|
+
if (best < 0) continue;
|
|
176
|
+
if (best !== active) {
|
|
177
|
+
// смена контракта: масштабируем так, чтобы первая свеча нового дня продолжила предыдущую цену
|
|
178
|
+
const c = raw[best];
|
|
179
|
+
let j = ptr[best];
|
|
180
|
+
while (j < c.n && dayKey(c.ts[j]) < d) j++;
|
|
181
|
+
if (j < c.n && dayKey(c.ts[j]) === d && Number.isFinite(prevClose) && c.close[j] > 0) factor = prevClose / c.close[j];
|
|
182
|
+
active = best;
|
|
183
|
+
}
|
|
184
|
+
const c = raw[active];
|
|
185
|
+
let j = ptr[active];
|
|
186
|
+
while (j < c.n && dayKey(c.ts[j]) < d) j++;
|
|
187
|
+
while (j < c.n && dayKey(c.ts[j]) === d) {
|
|
188
|
+
ts.push(c.ts[j]);
|
|
189
|
+
close.push(c.close[j] * factor);
|
|
190
|
+
prevClose = close[close.length - 1];
|
|
191
|
+
j++;
|
|
192
|
+
}
|
|
193
|
+
ptr[active] = j;
|
|
194
|
+
}
|
|
195
|
+
const n = ts.length;
|
|
196
|
+
return { n, ts: Float64Array.from(ts), open: Float64Array.from(close), close: Float64Array.from(close), value: new Float64Array(n) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Выровненная по времени «матрица рынка»: N акций × T свечей по 10 минут (только будни),
|
|
200
|
+
// цены с переносом последнего значения, обороты, доходности, рынок (IMOEX).
|
|
201
|
+
// Цены акций скорректированы на дивиденды (полная доходность; useDividends=false — без этого), макро — только при useMacro.
|
|
202
|
+
export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, useMacro = false, useDividends = true, localOnly = false, onProgress = () => {}, signal }) {
|
|
203
|
+
const key = cacheKey(interval, fromTs, tillTs, useMacro, useDividends);
|
|
204
|
+
const hit = datasetCache.get(key);
|
|
205
|
+
if (hit && Date.now() - hit.at < 30 * 60000) return hit.ds;
|
|
206
|
+
const vocab = await ensureVocab();
|
|
207
|
+
const symbols = vocab.map((v) => v.symbol);
|
|
208
|
+
const raw = {};
|
|
209
|
+
let done = 0;
|
|
210
|
+
const divStat = { applied: 0, skipped: 0 };
|
|
211
|
+
const macroIds = useMacro ? MACRO.filter((m) => m.id !== 'BR').map((m) => m.id) : [];
|
|
212
|
+
const all = [...symbols, 'IMOEX', ...macroIds];
|
|
213
|
+
// акция дольше 2 минут — пропускаем и останавливаем её загрузку (скачанное останется в кэше)
|
|
214
|
+
const WITHIN = 120000;
|
|
215
|
+
await pool(all, 4, async (s) => {
|
|
216
|
+
if (done % 8 === 0) await new Promise((r) => setTimeout(r, 1));
|
|
217
|
+
if (signal?.aborted) throw new Error('Отменено');
|
|
218
|
+
const symCtrl = new AbortController();
|
|
219
|
+
const effSignal = signal ? AbortSignal.any([signal, symCtrl.signal]) : symCtrl.signal;
|
|
220
|
+
const killer = setTimeout(() => symCtrl.abort(), WITHIN);
|
|
221
|
+
try {
|
|
222
|
+
// акциям подгружаем дивиденды и сразу корректируем цены на полную доходность
|
|
223
|
+
const divs = useDividends && symbols.includes(s) ? await ensureDividends(s, { signal: effSignal, localOnly }) : null;
|
|
224
|
+
const rows = await ensureCandles(s, interval, fromTs, tillTs, { signal: effSignal, localOnly });
|
|
225
|
+
// объекты свечей отпускаются сразу, а не копятся до конца загрузки всех акций
|
|
226
|
+
raw[s] = compact(rows);
|
|
227
|
+
if (divs?.length) {
|
|
228
|
+
// DIVIDENDS_MODE=raw — прежняя корректировка без сверки с ценой (только для сравнения результатов)
|
|
229
|
+
const st = adjustDividends(raw[s], divs, { verify: process.env.DIVIDENDS_MODE !== 'raw' });
|
|
230
|
+
divStat.applied += st.applied;
|
|
231
|
+
divStat.skipped += st.skipped;
|
|
232
|
+
}
|
|
233
|
+
} catch (e) {
|
|
234
|
+
if (signal?.aborted) throw new Error('Отменено');
|
|
235
|
+
raw[s] = EMPTY();
|
|
236
|
+
console.warn(`[dataset] ${s}: ${e.message} — акция пропущена`);
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(killer);
|
|
239
|
+
onProgress({ symbol: s, done: ++done, total: all.length });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
if (useDividends) console.log(`[dataset] дивиденды: учтено ${divStat.applied}, пропущено (нет подтверждающего падения цены) ${divStat.skipped}`);
|
|
243
|
+
// нефть Brent: склейка фьючерсов (много контрактов — отдельно от общего пула)
|
|
244
|
+
if (useMacro) {
|
|
245
|
+
try {
|
|
246
|
+
onProgress({ symbol: 'BR', done: done, total: all.length });
|
|
247
|
+
raw.BR = await loadBrent(fromTs, tillTs, signal);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if (signal?.aborted) throw new Error('Отменено');
|
|
250
|
+
raw.BR = EMPTY();
|
|
251
|
+
console.warn(`[dataset] BR: ${e.message} — нефть пропущена`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!raw.IMOEX?.n) throw new Error('Нет данных индекса МосБиржи');
|
|
255
|
+
|
|
256
|
+
// временная шкала: будни, где торговалось хотя бы 40% акций
|
|
257
|
+
const count = new Map();
|
|
258
|
+
for (const s of symbols) {
|
|
259
|
+
const c = raw[s];
|
|
260
|
+
for (let i = 0; i < c.n; i++) if (dow(c.ts[i]) % 6 !== 0) count.set(c.ts[i], (count.get(c.ts[i]) || 0) + 1);
|
|
261
|
+
}
|
|
262
|
+
const timeline = [...count.entries()].filter(([, n]) => n >= symbols.length * 0.4).map(([t]) => t).sort((a, b) => a - b);
|
|
263
|
+
const T = timeline.length;
|
|
264
|
+
const index = new Map(timeline.map((t, i) => [t, i]));
|
|
265
|
+
|
|
266
|
+
// open — цена первой сделки свечи (по ней реально можно войти после решения на закрытии предыдущей)
|
|
267
|
+
const build = (c) => {
|
|
268
|
+
const close = new Float64Array(T).fill(NaN), open = new Float64Array(T).fill(NaN), high = new Float64Array(T).fill(NaN), low = new Float64Array(T).fill(NaN), value = new Float64Array(T), has = new Uint8Array(T);
|
|
269
|
+
let j = 0, last = NaN;
|
|
270
|
+
for (let i = 0; i < T; i++) {
|
|
271
|
+
const t = timeline[i];
|
|
272
|
+
let o = NaN, h = NaN, l = NaN;
|
|
273
|
+
while (j < c.n && c.ts[j] <= t) {
|
|
274
|
+
if (c.ts[j] === t) { value[i] = c.value[j]; has[i] = 1; o = c.open[j]; h = c.high?.[j]; l = c.low?.[j]; }
|
|
275
|
+
last = c.close[j];
|
|
276
|
+
j++;
|
|
277
|
+
}
|
|
278
|
+
close[i] = last;
|
|
279
|
+
open[i] = Number.isFinite(o) ? o : last;
|
|
280
|
+
high[i] = Number.isFinite(h) && h > 0 ? h : last;
|
|
281
|
+
low[i] = Number.isFinite(l) && l > 0 ? l : last;
|
|
282
|
+
}
|
|
283
|
+
return { close, open, high, low, value, has };
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const tokens = [];
|
|
287
|
+
for (let vi = 0; vi < vocab.length; vi++) {
|
|
288
|
+
if (vi % 15 === 0) {
|
|
289
|
+
if (signal?.aborted) throw new Error('Отменено');
|
|
290
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
291
|
+
}
|
|
292
|
+
const v = vocab[vi];
|
|
293
|
+
const c = raw[v.symbol];
|
|
294
|
+
if (!c?.n) continue;
|
|
295
|
+
const s = build(c);
|
|
296
|
+
let cov = 0;
|
|
297
|
+
for (let i = 0; i < T; i++) cov += s.has[i];
|
|
298
|
+
if (cov < T * 0.6) continue; // слишком мало торгов (новые или неликвидные бумаги)
|
|
299
|
+
tokens.push({ symbol: v.symbol, name: v.name, weight: v.weight, ...s });
|
|
300
|
+
}
|
|
301
|
+
const mk = build(raw.IMOEX);
|
|
302
|
+
// макро-ряды: выровненные цены с переносом (доходности посчитаем после lret)
|
|
303
|
+
const macroRaw = [];
|
|
304
|
+
if (useMacro) {
|
|
305
|
+
for (const m of MACRO) {
|
|
306
|
+
const c = raw[m.id];
|
|
307
|
+
if (!c?.n) continue;
|
|
308
|
+
const s = build(c);
|
|
309
|
+
macroRaw.push({ id: m.id, label: m.label, s });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const k of Object.keys(raw)) delete raw[k]; // сырые компакт-массивы больше не нужны
|
|
313
|
+
|
|
314
|
+
// логарифмические доходности (0, если цены ещё нет) и временны́е циклы (час суток и день недели по МСК)
|
|
315
|
+
const lret = (close) => {
|
|
316
|
+
const r = new Float64Array(T);
|
|
317
|
+
for (let i = 1; i < T; i++) r[i] = close[i] > 0 && close[i - 1] > 0 ? Math.log(close[i] / close[i - 1]) : 0;
|
|
318
|
+
return r;
|
|
319
|
+
};
|
|
320
|
+
const hourMsk = new Uint8Array(T), dowMsk = new Uint8Array(T);
|
|
321
|
+
for (let i = 0; i < T; i++) {
|
|
322
|
+
const msk = timeline[i] + MSK;
|
|
323
|
+
hourMsk[i] = Math.floor((msk % DAY) / 3600000);
|
|
324
|
+
dowMsk[i] = (Math.floor(msk / DAY) + 4) % 7;
|
|
325
|
+
}
|
|
326
|
+
for (const tk of tokens) {
|
|
327
|
+
tk.ret = lret(tk.close);
|
|
328
|
+
// необычность оборота с учётом суточного цикла: оборот свечи сравнивается со средним оборотом
|
|
329
|
+
// этой же акции в ЭТОТ ЖЕ ЧАС СУТОК за прошлые торговые дни (с экспоненциальным затуханием, строго по прошлому)
|
|
330
|
+
const hSum = new Float64Array(24), hCnt = new Uint32Array(24);
|
|
331
|
+
tk.valRatio = new Float64Array(T).fill(1);
|
|
332
|
+
for (let i = 0; i < T; i++) {
|
|
333
|
+
const hs = hourMsk[i];
|
|
334
|
+
if (hCnt[hs] >= 12) {
|
|
335
|
+
const avg = hSum[hs] / hCnt[hs];
|
|
336
|
+
tk.valRatio[i] = avg > 0 ? tk.value[i] / avg : 1;
|
|
337
|
+
hSum[hs] *= 0.99;
|
|
338
|
+
hCnt[hs] = Math.round(hCnt[hs] * 0.99);
|
|
339
|
+
}
|
|
340
|
+
hSum[hs] += tk.value[i];
|
|
341
|
+
hCnt[hs]++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const mret = lret(mk.close);
|
|
345
|
+
// макро-ряды: лог-доходности с переносом цены (вне торгов — 0)
|
|
346
|
+
const macro = macroRaw.map((m) => ({ id: m.id, label: m.label, ret: lret(m.s.close), has: m.s.has }));
|
|
347
|
+
const days = new Set(timeline.map(dayKey)).size;
|
|
348
|
+
// ликвидность: в скольких из последних 20 свечей были сделки
|
|
349
|
+
for (const tk of tokens) {
|
|
350
|
+
tk.liq = new Uint8Array(T);
|
|
351
|
+
let n = 0;
|
|
352
|
+
for (let i = 0; i < T; i++) {
|
|
353
|
+
n += tk.has[i];
|
|
354
|
+
if (i >= 20) n -= tk.has[i - 20];
|
|
355
|
+
tk.liq[i] = n;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const ds = { tokens, symbols: tokens.map((t) => t.symbol), timeline, hourMsk, dowMsk, T, index, mret, mclose: mk.close, mopen: mk.open, macro, barsPerDay: days ? Math.round(T / days) : 54 };
|
|
359
|
+
datasetCache.set(key, { ds, at: Date.now() });
|
|
360
|
+
if (datasetCache.size > 2) datasetCache.delete(datasetCache.keys().next().value);
|
|
361
|
+
return ds;
|
|
362
|
+
}
|