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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import { PARAMS } from '../params-meta.js';
|
|
2
3
|
import { chatStream, activeModelInfo, isLlmEnabled } from '../llm/client.js';
|
|
3
4
|
import { hub } from '../hub.js';
|
|
@@ -54,7 +55,8 @@ const emitMsg = (m, type = 'chat:message') => hub.emit({ type, message: m });
|
|
|
54
55
|
// ---------- системная инструкция ----------
|
|
55
56
|
function systemPrompt(mode) {
|
|
56
57
|
const s = getSettings();
|
|
57
|
-
|
|
58
|
+
const PARAMS_VISIBLE = PARAMS.filter((p) => !p.hidden && !p.fixed);
|
|
59
|
+
const params = PARAMS_VISIBLE.map((p) => `- ${p.key} = ${JSON.stringify(s[p.key])} — ${p.label}${p.min !== undefined ? ` [${p.min}…${p.max}]` : ''}${p.ai === false ? ' (НЕ МЕНЯТЬ)' : ''}`).join('\n');
|
|
58
60
|
const journal = listJournal(30).reverse().map((j) => `- #${j.id} [${{ confirmed: 'подтвердилась', rejected: 'не подтвердилась', unclear: 'неясно' }[j.verdict] || j.verdict}] ${j.hypothesis}${j.change ? ` | изменение: ${j.change}` : ''} | итог: ${j.result}${j.run_ids?.length ? ` | проверки ${j.run_ids.map((x) => `№${x}`).join(', ')}` : ''}`).join('\n') || '(пусто — это первое исследование)';
|
|
59
61
|
const runs = recentRuns(6).map((r) => `- №${r.id} (${r.тип}, ${r.кто_запустил}${r.заметка ? `, «${r.заметка}»` : ''}): отличия ${JSON.stringify(r.отличия_от_умолчаний)}; ${JSON.stringify(r.период_настройки || r.результат || r.ошибка || r.статус)}`).join('\n') || '(исследований ещё не было)';
|
|
60
62
|
const ps = live.status();
|
|
@@ -76,16 +78,14 @@ function systemPrompt(mode) {
|
|
|
76
78
|
Задача — как языковая модель предсказывает следующий токен, предсказать следующее состояние: какие акции сдвинутся в ближайшие часы и куда.
|
|
77
79
|
Перетоки капитала видны как «волны» по карте: если двинулся сосед по карте, а акция ещё нет — она может догнать (или, наоборот, вернуться).
|
|
78
80
|
|
|
79
|
-
Исследование идёт лесенкой (инструмент run_model_research, сравнивает все модели сразу):
|
|
81
|
+
Исследование идёт лесенкой (инструмент run_model_research, сравнивает все три модели сразу):
|
|
80
82
|
- «своя инерция» — простейшая планка: акция предсказывается по своему недавнему движению;
|
|
81
83
|
- «кто за кем ходит» — линейная модель: каждая акция по недавним движениям всех остальных;
|
|
82
|
-
- «внимание через карту» — влияние соседей по карте рынка (12 «голов внимания»: движение цены или денежный поток × 3 масштаба времени × широкое/узкое внимание);
|
|
83
|
-
- «трансформер» (ступень 3, только при modelUseTransformer=true или modelType='transformer') — небольшая нейросеть: 2 слоя внимания между акциями, обучаемый эмбеддинг каждой акции, учится заново в каждом окне с ранней остановкой. Исследование с ним в разы дольше; данных мало, легко переобучается — верь ему, только если он обыгрывает «внимание через карту» и на контрольном периоде.
|
|
84
|
-
Его настройки: transformerEpochs (эпох, 2–40, по умолчанию 8) и transformerStride (шаг свечей, 1–24, по умолчанию 6) меняют результат; больше эпох / меньше шаг — дольше. transformerParallel (окна одновременно на ядрах) и transformerGpu (видеокарта, экспериментально) — только про скорость, их без просьбы пользователя не трогай.
|
|
84
|
+
- «внимание через карту» — основная торгующая стратегия: влияние соседей по карте рынка (12 «голов внимания»: движение цены или денежный поток с почасовым суточным профилем объёма × 3 масштаба времени × широкое/узкое внимание);
|
|
85
85
|
Метрики модели: «совпадение прогноза» (ранговая корреляция прогноза с фактом по акциям, 0.03+ уже ценно), доля недель с плюсом,
|
|
86
86
|
доход портфеля из лучших прогнозов с комиссиями и БЕЗ комиссий (разница показывает, сколько съедают издержки), сравнение с рынком.
|
|
87
87
|
Сделки исполняются по цене открытия следующей свечи (честно). Порог выгоды modelMinEdge: покупаем, только если ожидаемое движение больше стоимости сделки × порог.
|
|
88
|
-
У стратегии включено самообучение правил торговли (modelAutoAdapt=true по умолчанию
|
|
88
|
+
У стратегии включено самообучение правил торговли (modelAutoAdapt=true по умолчанию — Полный Автомат): мульти-горизонтное обучение (1.5–3.5 ч), сопровождение на полушаге H/2 (мягкий порог 0.35 на первых двух полушагах и 0.95 по зрелости), взвешивание слота по силе сигнала (Conviction Sizing), адаптация порогов покупок и продаж к среднесрочному тренду индекса Мосбиржи и временный карантин убыточных «акций-убийц» с затуханием. Дополнительно есть настройки: число акций (modelTopK), направление (modelDirection), способ распределения долей (modelAllocMode: equal / invvol), фильтр падающего рынка (modelMarketFilter: off / ma), а при выключенном modelAutoAdapt — ручные горизонт (modelHorizon), порог входа (modelMinEdge) и порог выхода (modelExitRatio).
|
|
89
89
|
Направление modelDirection: long — только покупки (по умолчанию); longshort — ещё и продажи без покупки (шорт) акций, ожидаемо слабее рынка (ожидание ниже −порога); neutral — парами: равные суммы покупок и продаж, нет пары — не торгуем; общий рост или падение рынка почти не влияют, результат сравнивай с нулём, а не с рынком. С продажами на позицию приходится капитал/2К. За продажу брокер берёт плату за заём акций modelShortFeePct (% годовых за время удержания). В разборах есть вклад покупок и продаж и разбивка по стороне — смотри, какая сторона зарабатывает.
|
|
90
90
|
Разница в итоге меньше 5 п.п. между двумя исследованиями обычно в пределах случайности — не называй её надёжной без контрольного периода.
|
|
91
91
|
model_forecast — прогноз сейчас, market_neighbors — соседи акции на карте.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import { PARAMS, PARAM_MAP } from '../params-meta.js';
|
|
2
3
|
import { getSettings, updateSettings, validateChanges } from '../settings.js';
|
|
3
4
|
import { getRun, listRuns, addJournal, listJournal, kvGet, kvSet } from '../db.js';
|
|
@@ -15,7 +16,7 @@ const AI_LOCKED = PARAMS.filter((p) => p.ai === false).map((p) => p.key);
|
|
|
15
16
|
const changesSchema = {
|
|
16
17
|
type: 'object',
|
|
17
18
|
description: 'Изменения настроек: ключ — точное название настройки из списка в системной инструкции (например modelHorizon, modelMinEdge), значение — новое значение.',
|
|
18
|
-
properties: Object.fromEntries(PARAMS.filter((p) => p.ai !== false).map((p) => [p.key, {
|
|
19
|
+
properties: Object.fromEntries(PARAMS.filter((p) => p.ai !== false && !p.hidden && !p.fixed).map((p) => [p.key, {
|
|
19
20
|
type: p.type === 'checkbox' ? 'boolean' : p.type === 'select' ? 'string' : 'number',
|
|
20
21
|
description: `${p.label}${p.min !== undefined ? ` (${p.min}–${p.max})` : ''}`,
|
|
21
22
|
}])),
|
package/server/util.js
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="ru">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<title>Обучение трансформера на видеокарте</title>
|
|
6
|
-
<!-- служебная страница: открывается безголовым браузером, человеку здесь смотреть не на что -->
|
|
7
|
-
<script src="/tf/tf.min.js"></script>
|
|
8
|
-
<script type="module" src="/gpu/gpu-worker.js"></script>
|
|
9
|
-
</head>
|
|
10
|
-
<body>Обучение трансформера на видеокарте</body>
|
|
11
|
-
</html>
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Браузерная часть обучения на видеокарте: подключается к серверу, сообщает, какая видеокарта,
|
|
2
|
-
// и обучает окна тем же кодом, что и в Node (tf — глобальный, из tf.min.js; движок WebGL).
|
|
3
|
-
import { fitPredictCore, pack, unpack } from './transformer-core.js';
|
|
4
|
-
|
|
5
|
-
/* global tf */
|
|
6
|
-
const token = new URLSearchParams(location.search).get('token');
|
|
7
|
-
const ws = new WebSocket(`ws://${location.host}/gpu-ws?token=${token}`);
|
|
8
|
-
ws.binaryType = 'arraybuffer';
|
|
9
|
-
let cancelled = false;
|
|
10
|
-
|
|
11
|
-
// Какая видеокарта реально рисует: WEBGL_debug_renderer_info (программную отрисовку сервер отбракует)
|
|
12
|
-
function gpuInfo() {
|
|
13
|
-
const c = document.createElement('canvas');
|
|
14
|
-
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
|
15
|
-
if (!gl) return { webgl: false };
|
|
16
|
-
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
17
|
-
return {
|
|
18
|
-
webgl: true,
|
|
19
|
-
renderer: String(ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER)),
|
|
20
|
-
vendor: String(ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR)),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
ws.onopen = async () => {
|
|
25
|
-
const info = gpuInfo();
|
|
26
|
-
let backend = null, float32 = false, error = null;
|
|
27
|
-
if (info.webgl) {
|
|
28
|
-
try {
|
|
29
|
-
tf.enableProdMode();
|
|
30
|
-
if (await tf.setBackend('webgl')) backend = tf.getBackend();
|
|
31
|
-
await tf.ready();
|
|
32
|
-
float32 = !!tf.env().getBool('WEBGL_RENDER_FLOAT32_ENABLED');
|
|
33
|
-
} catch (e) { error = e.message; }
|
|
34
|
-
}
|
|
35
|
-
ws.send(JSON.stringify({ type: 'hello', ...info, backend, float32, error }));
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
ws.onmessage = async (ev) => {
|
|
39
|
-
if (typeof ev.data === 'string') {
|
|
40
|
-
if (JSON.parse(ev.data).type === 'cancel') cancelled = true;
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const m = unpack(ev.data);
|
|
44
|
-
const { id } = m;
|
|
45
|
-
try {
|
|
46
|
-
const r = await fitPredictCore(tf, m.input, m.us, {
|
|
47
|
-
cfg: m.cfg,
|
|
48
|
-
isCancelled: () => cancelled,
|
|
49
|
-
onProgress: (x) => ws.send(JSON.stringify({ type: 'progress', id, ...x })),
|
|
50
|
-
});
|
|
51
|
-
const N = m.input.N, us = Int32Array.from(r.preds.keys());
|
|
52
|
-
const flat = new Float64Array(us.length * N);
|
|
53
|
-
us.forEach((u, k) => flat.set(r.preds.get(u), k * N));
|
|
54
|
-
ws.send(pack({ id, us, flat, info: { ...r.info, tensors: tf.memory().numTensors, backend: tf.getBackend() } }));
|
|
55
|
-
} catch (e) {
|
|
56
|
-
ws.send(JSON.stringify({ type: 'error', id, error: e.message, cancelled }));
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
ws.onclose = () => window.close();
|
package/server/model/tf-node.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Загрузка TensorFlow.js в Node (главный поток или рабочий поток): WASM, если не вышло — обычный процессорный движок.
|
|
2
|
-
// Отдельный модуль без других зависимостей проекта: его грузят рабочие потоки, которым не нужна база и биржа.
|
|
3
|
-
let tfReady = null;
|
|
4
|
-
export function loadTf() {
|
|
5
|
-
if (!tfReady) tfReady = (async () => {
|
|
6
|
-
// tfjs в Node при загрузке советует поставить tfjs-node — нативную сборку мы сознательно не используем
|
|
7
|
-
const warn = console.warn;
|
|
8
|
-
console.warn = (...a) => { if (!String(a[0] ?? '').includes('tfjs-node')) warn(...a); };
|
|
9
|
-
let tf;
|
|
10
|
-
try {
|
|
11
|
-
tf = await import('@tensorflow/tfjs');
|
|
12
|
-
tf.enableProdMode();
|
|
13
|
-
let ok = false;
|
|
14
|
-
try {
|
|
15
|
-
await import('@tensorflow/tfjs-backend-wasm');
|
|
16
|
-
ok = await tf.setBackend('wasm');
|
|
17
|
-
} catch { ok = false; }
|
|
18
|
-
if (!ok) await tf.setBackend('cpu');
|
|
19
|
-
await tf.ready();
|
|
20
|
-
} finally {
|
|
21
|
-
console.warn = warn;
|
|
22
|
-
}
|
|
23
|
-
return tf;
|
|
24
|
-
})().catch((e) => { tfReady = null; throw e; });
|
|
25
|
-
return tfReady;
|
|
26
|
-
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
// Ядро трансформера: обучение и прогноз. Один и тот же код работает и в Node (главный поток, рабочие потоки),
|
|
2
|
-
// и в браузере (обучение на видеокарте через WebGL), поэтому здесь нет импортов Node — tf передаётся параметром.
|
|
3
|
-
//
|
|
4
|
-
// Модель: в момент t каждая акция — «токен»: её признаки контекста (+ обучаемый эмбеддинг акции);
|
|
5
|
-
// слои энкодера смешивают информацию между акциями; на выходе — прогноз нормированной цели yz для каждой акции.
|
|
6
|
-
//
|
|
7
|
-
// На вход — «облегчённое окно» (см. transformerInput в transformer.js): только нужные для обучения массивы
|
|
8
|
-
// { N, W, H, X, liq, has, yz, macro: { list: [{ b, zs }] } | null, tr: моменты обучения, liqMin }.
|
|
9
|
-
import { mulberry32, spearman } from './linalg.js';
|
|
10
|
-
|
|
11
|
-
export const TF_CFG = {
|
|
12
|
-
d: 24, heads: 2, layers: 2, ff: 48, dropout: 0.1,
|
|
13
|
-
lr: 2e-3, weightDecay: 0.1, batch: 32, epochs: 8, patience: 2,
|
|
14
|
-
// соседние 10-минутные моменты почти одинаковы: в эпоху берём каждую 6-ю свечу,
|
|
15
|
-
// со сдвигом от эпохи к эпохе (за 6 эпох модель видит все моменты); проверка — каждая 3-я свеча
|
|
16
|
-
stride: 6, valStride: 3,
|
|
17
|
-
valFrac: 0.2, seed: 20261013,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Короткая пауза, чтобы успели обработаться другие события (отмена, прогресс).
|
|
21
|
-
// В Node — setImmediate, в браузере — MessageChannel (setTimeout в браузере медленнее)
|
|
22
|
-
const tick = (() => {
|
|
23
|
-
if (typeof setImmediate === 'function') return () => new Promise((r) => setImmediate(r));
|
|
24
|
-
const ch = new MessageChannel();
|
|
25
|
-
const q = [];
|
|
26
|
-
ch.port1.onmessage = () => q.shift()?.();
|
|
27
|
-
return () => new Promise((r) => { q.push(r); ch.port2.postMessage(0); });
|
|
28
|
-
})();
|
|
29
|
-
|
|
30
|
-
// Признаки акции i в момент u: 6 признаков контекста, флаги ликвидности, макро (бета акции × движение макро)
|
|
31
|
-
function featCount(f) {
|
|
32
|
-
return 6 + 2 + (f.macro ? f.macro.list.length * 3 : 0);
|
|
33
|
-
}
|
|
34
|
-
function fillFeat(f, u, out, off) {
|
|
35
|
-
const { N } = f;
|
|
36
|
-
const ML = f.macro ? f.macro.list : [];
|
|
37
|
-
const F = featCount(f);
|
|
38
|
-
for (let i = 0; i < N; i++) {
|
|
39
|
-
const k = u * N + i, o = off + i * F;
|
|
40
|
-
for (let s = 0; s < 3; s++) { out[o + s] = f.X[0][s][k]; out[o + 3 + s] = f.X[1][s][k]; }
|
|
41
|
-
out[o + 6] = f.liq[k] / 20;
|
|
42
|
-
out[o + 7] = f.has[k];
|
|
43
|
-
for (let m = 0; m < ML.length; m++) for (let s = 0; s < 3; s++) out[o + 8 + m * 3 + s] = ML[m].b[i] * ML[m].zs[s * f.W + u];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const liquidAt = (f, u, i) => f.has[u * f.N + i] && f.liq[u * f.N + i] >= f.liqMin;
|
|
47
|
-
|
|
48
|
-
// Нормировка признаков — среднее и разброс только по моментам окна обучения (ликвидные акции)
|
|
49
|
-
function featStats(f, us) {
|
|
50
|
-
const N = f.N, F = featCount(f);
|
|
51
|
-
const buf = new Float32Array(N * F);
|
|
52
|
-
const s1 = new Float64Array(F), s2 = new Float64Array(F);
|
|
53
|
-
let n = 0;
|
|
54
|
-
for (const u of us) {
|
|
55
|
-
fillFeat(f, u, buf, 0);
|
|
56
|
-
for (let i = 0; i < N; i++) {
|
|
57
|
-
if (!liquidAt(f, u, i)) continue;
|
|
58
|
-
n++;
|
|
59
|
-
for (let c = 0; c < F; c++) { const v = buf[i * F + c]; s1[c] += v; s2[c] += v * v; }
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const mean = new Float32Array(F), inv = new Float32Array(F);
|
|
63
|
-
for (let c = 0; c < F; c++) {
|
|
64
|
-
mean[c] = n ? s1[c] / n : 0;
|
|
65
|
-
const sd = n ? Math.sqrt(Math.max(s2[c] / n - mean[c] ** 2, 0)) : 1;
|
|
66
|
-
inv[c] = sd > 1e-6 ? 1 / sd : 0;
|
|
67
|
-
}
|
|
68
|
-
// флаги ликвидности не центрируем — только масштаб
|
|
69
|
-
mean[6] = 0; mean[7] = 0; inv[6] = 1; inv[7] = 1;
|
|
70
|
-
return { mean, inv };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Цель момента: yz, поперечно стандартизированная по ликвидным акциям (модель учит порядок «кто сильнее»)
|
|
74
|
-
function targetAt(f, u, y, lm, off) {
|
|
75
|
-
const N = f.N;
|
|
76
|
-
let s = 0, s2 = 0, n = 0;
|
|
77
|
-
for (let i = 0; i < N; i++) if (liquidAt(f, u, i)) { const v = f.yz[u * N + i]; s += v; s2 += v * v; n++; }
|
|
78
|
-
if (n < 5) { for (let i = 0; i < N; i++) { y[off + i] = 0; lm[off + i] = 0; } return null; }
|
|
79
|
-
const m = s / n, sd = Math.sqrt(Math.max(s2 / n - m * m, 1e-9));
|
|
80
|
-
for (let i = 0; i < N; i++) {
|
|
81
|
-
const ok = liquidAt(f, u, i);
|
|
82
|
-
lm[off + i] = ok ? 1 : 0;
|
|
83
|
-
y[off + i] = ok ? Math.max(-5, Math.min(5, (f.yz[u * N + i] - m) / sd)) : 0;
|
|
84
|
-
}
|
|
85
|
-
return sd;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function makeBatch(f, us, norm) {
|
|
89
|
-
const N = f.N, F = featCount(f), B = us.length;
|
|
90
|
-
const x = new Float32Array(B * N * F), km = new Float32Array(B * N);
|
|
91
|
-
us.forEach((u, b) => {
|
|
92
|
-
fillFeat(f, u, x, b * N * F);
|
|
93
|
-
for (let i = 0; i < N; i++) {
|
|
94
|
-
const o = (b * N + i) * F;
|
|
95
|
-
for (let c = 0; c < F; c++) x[o + c] = Math.max(-6, Math.min(6, (x[o + c] - norm.mean[c]) * norm.inv[c]));
|
|
96
|
-
// ключи внимания — акции, торговавшиеся в последние 20 свечей
|
|
97
|
-
km[b * N + i] = f.liq[u * N + i] > 0 ? 0 : -1e9;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
return { x, km, B };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function initWeights(tf, cfg, F, N) {
|
|
104
|
-
const { d, ff, layers } = cfg;
|
|
105
|
-
let seed = cfg.seed;
|
|
106
|
-
const glorot = (shape) => tf.variable(tf.initializers.glorotUniform({ seed: seed++ }).apply(shape));
|
|
107
|
-
const zeros = (n) => tf.variable(tf.zeros([n]));
|
|
108
|
-
const ones = (n) => tf.variable(tf.ones([n]));
|
|
109
|
-
const w = {
|
|
110
|
-
Win: glorot([F, d]), bin: zeros(d),
|
|
111
|
-
E: tf.variable(tf.initializers.randomNormal({ mean: 0, stddev: 0.02, seed: seed++ }).apply([N, d])),
|
|
112
|
-
L: [],
|
|
113
|
-
lnfg: ones(d), lnfb: zeros(d),
|
|
114
|
-
Wout: glorot([d, 1]), bout: zeros(1),
|
|
115
|
-
Wlin: tf.variable(tf.zeros([F, 1])),
|
|
116
|
-
};
|
|
117
|
-
for (let l = 0; l < layers; l++) {
|
|
118
|
-
w.L.push({
|
|
119
|
-
ln1g: ones(d), ln1b: zeros(d),
|
|
120
|
-
Wq: glorot([d, d]), Wk: glorot([d, d]), Wv: glorot([d, d]), Wo: glorot([d, d]), bo: zeros(d),
|
|
121
|
-
ln2g: ones(d), ln2b: zeros(d),
|
|
122
|
-
W1: glorot([d, ff]), b1: zeros(ff), W2: glorot([ff, d]), b2: zeros(d),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
return w;
|
|
126
|
-
}
|
|
127
|
-
const allVars = (w) => [w.Win, w.bin, w.E, w.lnfg, w.lnfb, w.Wout, w.bout, w.Wlin, ...w.L.flatMap((l) => Object.values(l))];
|
|
128
|
-
// затухание весов — только у матриц (не у сдвигов и норм)
|
|
129
|
-
const decayVars = (w) => [w.Win, w.E, w.Wout, w.Wlin, ...w.L.flatMap((l) => [l.Wq, l.Wk, l.Wv, l.Wo, l.W1, l.W2])];
|
|
130
|
-
|
|
131
|
-
// Прямой проход: x [B,N,F], km [B,N] (0 или −1e9) → прогноз [B,N]
|
|
132
|
-
function forward(tf, w, cfg, x, km, B, N, train, seedRef) {
|
|
133
|
-
const { d, heads } = cfg;
|
|
134
|
-
const dh = d / heads;
|
|
135
|
-
const drop = (t) => (train && cfg.dropout > 0 ? tf.dropout(t, cfg.dropout, undefined, seedRef.v++) : t);
|
|
136
|
-
const ln = (t, g, b) => {
|
|
137
|
-
const { mean, variance } = tf.moments(t, -1, true);
|
|
138
|
-
return t.sub(mean).div(variance.add(1e-5).sqrt()).mul(g).add(b);
|
|
139
|
-
};
|
|
140
|
-
const lin = (t2, W, b) => (b ? tf.matMul(t2, W).add(b) : tf.matMul(t2, W));
|
|
141
|
-
const F = x.shape[2];
|
|
142
|
-
let h = lin(x.reshape([B * N, F]), w.Win, w.bin).reshape([B, N, d]).add(w.E);
|
|
143
|
-
const mask = km.reshape([B, 1, 1, N]);
|
|
144
|
-
const split = (t2) => t2.reshape([B, N, heads, dh]).transpose([0, 2, 1, 3]);
|
|
145
|
-
for (const L of w.L) {
|
|
146
|
-
const a = ln(h, L.ln1g, L.ln1b).reshape([B * N, d]);
|
|
147
|
-
const q = split(lin(a, L.Wq).mul(1 / Math.sqrt(dh))), k = split(lin(a, L.Wk)), v = split(lin(a, L.Wv));
|
|
148
|
-
// dropout — на выходах внимания и MLP (на самих весах внимания он втрое дороже по времени)
|
|
149
|
-
const att = tf.softmax(tf.matMul(q, k, false, true).add(mask));
|
|
150
|
-
const o = tf.matMul(att, v).transpose([0, 2, 1, 3]).reshape([B * N, d]);
|
|
151
|
-
h = h.add(drop(lin(o, L.Wo, L.bo)).reshape([B, N, d]));
|
|
152
|
-
const a2 = ln(h, L.ln2g, L.ln2b).reshape([B * N, d]);
|
|
153
|
-
const m = lin(tf.relu(lin(a2, L.W1, L.b1)), L.W2, L.b2);
|
|
154
|
-
h = h.add(drop(m).reshape([B, N, d]));
|
|
155
|
-
}
|
|
156
|
-
// + линейная «обходная» часть прямо от признаков: LayerNorm сглаживает величину движения,
|
|
157
|
-
// а для порога выгоды важны именно крупные прогнозы
|
|
158
|
-
return lin(ln(h, w.lnfg, w.lnfb).reshape([B * N, d]), w.Wout, w.bout).add(tf.matMul(x.reshape([B * N, F]), w.Wlin)).reshape([B, N]);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Прогноз по моментам us: Map(u → Float64Array(N)) в единицах yz
|
|
162
|
-
export function predictCore(tf, model, f, us) {
|
|
163
|
-
const N = f.N, F = featCount(f);
|
|
164
|
-
const out = new Map();
|
|
165
|
-
const step = 64;
|
|
166
|
-
for (let s = 0; s < us.length; s += step) {
|
|
167
|
-
const part = Array.from(us.slice(s, s + step));
|
|
168
|
-
const { x, km, B } = makeBatch(f, part, model.norm);
|
|
169
|
-
const t = tf.tidy(() => forward(tf, model.w, model.cfg, tf.tensor3d(x, [B, N, F]), tf.tensor2d(km, [B, N]), B, N, false, null));
|
|
170
|
-
const pr = t.dataSync();
|
|
171
|
-
t.dispose();
|
|
172
|
-
part.forEach((u, b) => {
|
|
173
|
-
const r = new Float64Array(N);
|
|
174
|
-
for (let i = 0; i < N; i++) r[i] = pr[b * N + i] * model.scale;
|
|
175
|
-
out.set(u, r);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const meanIc = (f, preds) => {
|
|
182
|
-
let s = 0, n = 0;
|
|
183
|
-
for (const [u, pr] of preds) {
|
|
184
|
-
const ok = [...pr.keys()].filter((i) => liquidAt(f, u, i));
|
|
185
|
-
if (ok.length < 5) continue;
|
|
186
|
-
s += spearman(ok.map((i) => pr[i]), ok.map((i) => f.yz[u * f.N + i])); n++;
|
|
187
|
-
}
|
|
188
|
-
return n ? s / n : 0;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Обучение в окне. onProgress({ epoch, epochs }) — в начале каждой эпохи; isCancelled() → прерывание между батчами
|
|
192
|
-
export async function fitCore(tf, f, { onProgress = () => {}, isCancelled = () => false, cfg: over = {} } = {}) {
|
|
193
|
-
const cfg = { ...TF_CFG, ...over };
|
|
194
|
-
const N = f.N, F = featCount(f);
|
|
195
|
-
// только моменты, цель которых целиком внутри окна обучения
|
|
196
|
-
const all = Array.from(f.tr);
|
|
197
|
-
const nVal = Math.max(1, Math.floor(all.length * cfg.valFrac));
|
|
198
|
-
// проверка для ранней остановки — последние 20% окна обучения
|
|
199
|
-
const valUs = all.slice(all.length - nVal).filter((u, k) => k % cfg.valStride === 0);
|
|
200
|
-
// зазор H+1 свечей: цели обучающих моментов не заходят в период проверки
|
|
201
|
-
const trUs0 = all.slice(0, all.length - nVal).filter((u) => u + f.H + 1 < valUs[0]);
|
|
202
|
-
const norm = featStats(f, all.filter((u, k) => k % cfg.valStride === 0));
|
|
203
|
-
|
|
204
|
-
// цели обучения заранее (поперечная стандартизация по моменту)
|
|
205
|
-
const Y = new Float32Array(trUs0.length * N), LM = new Float32Array(trUs0.length * N);
|
|
206
|
-
const trUs = [];
|
|
207
|
-
const rowOf = new Map();
|
|
208
|
-
trUs0.forEach((u, k) => {
|
|
209
|
-
if (targetAt(f, u, Y, LM, k * N) === null) return;
|
|
210
|
-
rowOf.set(u, k);
|
|
211
|
-
trUs.push(u);
|
|
212
|
-
});
|
|
213
|
-
if (trUs.length < cfg.batch * cfg.stride) throw new Error('Трансформер: слишком мало моментов для обучения');
|
|
214
|
-
|
|
215
|
-
// tidy освобождает начальные тензоры инициализаторов (переменные остаются)
|
|
216
|
-
const w = tf.tidy(() => initWeights(tf, cfg, F, N));
|
|
217
|
-
const vars = allVars(w), dvars = decayVars(w);
|
|
218
|
-
const nParams = vars.reduce((s, v) => s + v.size, 0);
|
|
219
|
-
const opt = tf.train.adam(cfg.lr);
|
|
220
|
-
const rnd = mulberry32(cfg.seed);
|
|
221
|
-
const seedRef = { v: cfg.seed * 7 };
|
|
222
|
-
// scale — множитель выхода; при обучении 1 (единицы поперечного разброса), после — калибровка
|
|
223
|
-
const model = { name: 'transformer', w, cfg, norm, scale: 1, nParams };
|
|
224
|
-
let best = null, bad = 0, epochsRun = 0;
|
|
225
|
-
const snapshot = () => vars.map((v) => tf.keep(v.clone()));
|
|
226
|
-
const freeSnap = (s) => s && s.forEach((t) => t.dispose());
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
for (let ep = 0; ep < cfg.epochs; ep++) {
|
|
230
|
-
onProgress({ epoch: ep + 1, epochs: cfg.epochs });
|
|
231
|
-
await tick();
|
|
232
|
-
// фиксированный (по seed) порядок батчей
|
|
233
|
-
const order = trUs.filter((u, k) => k % cfg.stride === ep % cfg.stride);
|
|
234
|
-
for (let i = order.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [order[i], order[j]] = [order[j], order[i]]; }
|
|
235
|
-
for (let s = 0; s + cfg.batch <= order.length; s += cfg.batch) {
|
|
236
|
-
if (isCancelled()) throw new Error('Отменено');
|
|
237
|
-
const part = order.slice(s, s + cfg.batch);
|
|
238
|
-
const B = part.length;
|
|
239
|
-
const { x, km } = makeBatch(f, part, norm);
|
|
240
|
-
const y = new Float32Array(B * N), lm = new Float32Array(B * N);
|
|
241
|
-
part.forEach((u, b) => { const r = rowOf.get(u) * N; y.set(Y.subarray(r, r + N), b * N); lm.set(LM.subarray(r, r + N), b * N); });
|
|
242
|
-
tf.tidy(() => {
|
|
243
|
-
const xt = tf.tensor3d(x, [B, N, F]), kt = tf.tensor2d(km, [B, N]);
|
|
244
|
-
const yt = tf.tensor2d(y, [B, N]), mt = tf.tensor2d(lm, [B, N]);
|
|
245
|
-
// MSE только по ликвидным акциям
|
|
246
|
-
opt.minimize(() => forward(tf, w, cfg, xt, kt, B, N, true, seedRef).sub(yt).square().mul(mt).sum().div(mt.sum().maximum(1)), false, vars);
|
|
247
|
-
// AdamW: затухание весов отдельно от градиента
|
|
248
|
-
const k = 1 - cfg.lr * cfg.weightDecay;
|
|
249
|
-
for (const v of dvars) v.assign(v.mul(k));
|
|
250
|
-
});
|
|
251
|
-
if ((s / cfg.batch) % 4 === 3) await tick();
|
|
252
|
-
}
|
|
253
|
-
epochsRun = ep + 1;
|
|
254
|
-
// ранняя остановка по совпадению прогноза с целью на последних 20% окна обучения
|
|
255
|
-
const valIc = meanIc(f, predictCore(tf, model, f, valUs));
|
|
256
|
-
if (!best || valIc > best.valIc) {
|
|
257
|
-
freeSnap(best?.snap);
|
|
258
|
-
best = { valIc, epoch: ep + 1, snap: snapshot() };
|
|
259
|
-
bad = 0;
|
|
260
|
-
} else if (++bad >= cfg.patience) break;
|
|
261
|
-
}
|
|
262
|
-
// возвращаем веса лучшей эпохи
|
|
263
|
-
if (best) vars.forEach((v, k) => v.assign(best.snap[k]));
|
|
264
|
-
// калибровка масштаба на проверочной части окна обучения: прогноз в единицах yz (как у гребневых моделей),
|
|
265
|
-
// иначе осторожная нейросеть редко преодолевает «Порог выгоды». Отрицательный наклон → прогноз 0.
|
|
266
|
-
model.scale = 1;
|
|
267
|
-
let sxy = 0, sxx = 0;
|
|
268
|
-
for (const [u, pr] of predictCore(tf, model, f, valUs)) {
|
|
269
|
-
const ok = [...pr.keys()].filter((i) => liquidAt(f, u, i));
|
|
270
|
-
if (ok.length < 5) continue;
|
|
271
|
-
const mp = ok.reduce((s, i) => s + pr[i], 0) / ok.length, my = ok.reduce((s, i) => s + f.yz[u * N + i], 0) / ok.length;
|
|
272
|
-
for (const i of ok) { const a = pr[i] - mp; sxy += a * (f.yz[u * N + i] - my); sxx += a * a; }
|
|
273
|
-
}
|
|
274
|
-
model.scale = sxx > 0 ? Math.max(0, sxy / sxx) : 0;
|
|
275
|
-
} catch (e) {
|
|
276
|
-
freeSnap(best?.snap);
|
|
277
|
-
vars.forEach((v) => v.dispose());
|
|
278
|
-
opt.dispose();
|
|
279
|
-
throw e;
|
|
280
|
-
}
|
|
281
|
-
freeSnap(best?.snap);
|
|
282
|
-
opt.dispose();
|
|
283
|
-
model.info = { params: nParams, epochs: epochsRun, bestEpoch: best?.epoch ?? 0, valIc: best?.valIc ?? null, scale: model.scale, trainMoments: trUs.length, valMoments: valUs.length };
|
|
284
|
-
model.dispose = () => vars.forEach((v) => v.dispose());
|
|
285
|
-
return model;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Обучение и сразу прогноз на моменты месяца прогноза: { preds: Map(u → Float64Array), info }
|
|
289
|
-
export async function fitPredictCore(tf, f, us, opts) {
|
|
290
|
-
const model = await fitCore(tf, f, opts);
|
|
291
|
-
try {
|
|
292
|
-
const preds = predictCore(tf, model, f, us);
|
|
293
|
-
return { preds, info: model.info };
|
|
294
|
-
} finally {
|
|
295
|
-
model.dispose();
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ---------- упаковка в один двоичный блок (для передачи через WebSocket) ----------
|
|
300
|
-
// [4 байта: длина заголовка][заголовок JSON][выравнивание до 8][массивы подряд, каждый с выравниванием до 8]
|
|
301
|
-
const TA = { Float64Array, Float32Array, Int32Array, Uint32Array, Uint8Array, Int16Array, Uint16Array, Int8Array };
|
|
302
|
-
export function pack(obj) {
|
|
303
|
-
const arrs = [];
|
|
304
|
-
let off = 0;
|
|
305
|
-
const header = JSON.stringify(obj, (k, v) => {
|
|
306
|
-
if (ArrayBuffer.isView(v) && !(v instanceof DataView)) {
|
|
307
|
-
const rec = { $ta: v.constructor.name, off, len: v.length };
|
|
308
|
-
arrs.push([v, off]);
|
|
309
|
-
off += Math.ceil(v.byteLength / 8) * 8;
|
|
310
|
-
return rec;
|
|
311
|
-
}
|
|
312
|
-
return v;
|
|
313
|
-
});
|
|
314
|
-
const hb = new TextEncoder().encode(header);
|
|
315
|
-
const base = Math.ceil((4 + hb.length) / 8) * 8;
|
|
316
|
-
const out = new Uint8Array(base + off);
|
|
317
|
-
new DataView(out.buffer).setUint32(0, hb.length, true);
|
|
318
|
-
out.set(hb, 4);
|
|
319
|
-
for (const [v, o] of arrs) out.set(new Uint8Array(v.buffer, v.byteOffset, v.byteLength), base + o);
|
|
320
|
-
return out;
|
|
321
|
-
}
|
|
322
|
-
export function unpack(data) {
|
|
323
|
-
// копия в собственный буфер: так массивы гарантированно выровнены
|
|
324
|
-
const u8 = new Uint8Array(data.byteLength);
|
|
325
|
-
u8.set(data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
326
|
-
const hl = new DataView(u8.buffer).getUint32(0, true);
|
|
327
|
-
const base = Math.ceil((4 + hl) / 8) * 8;
|
|
328
|
-
const header = new TextDecoder().decode(u8.subarray(4, 4 + hl));
|
|
329
|
-
return JSON.parse(header, (k, v) => (v && typeof v === 'object' && v.$ta ? new TA[v.$ta](u8.buffer, base + v.off, v.len) : v));
|
|
330
|
-
}
|