neuroalpha 0.0.0-stage → 0.2.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 +164 -3
- package/bin/cli.js +69 -0
- package/config/strategy.json +33 -0
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +156 -0
- package/package.json +43 -6
- package/public/app.js +2410 -0
- package/public/index.html +337 -0
- package/public/landing.html +870 -0
- package/public/legal.html +90 -0
- package/public/styles.css +343 -0
- package/server/api/routes.js +333 -0
- package/server/auth/index.js +340 -0
- package/server/billing/yookassa.js +132 -0
- package/server/brokers/index.js +645 -0
- package/server/config.js +44 -0
- package/server/data/moex.js +165 -0
- package/server/db.js +420 -0
- package/server/hub.js +14 -0
- package/server/index.js +92 -0
- package/server/llm/client.js +187 -0
- package/server/llm/providers.js +355 -0
- package/server/model/dataset.js +352 -0
- package/server/model/forecast.js +121 -0
- package/server/model/gpu-worker.html +11 -0
- package/server/model/gpu-worker.js +60 -0
- package/server/model/linalg.js +142 -0
- package/server/model/live.js +486 -0
- package/server/model/metrics.js +134 -0
- package/server/model/models.js +459 -0
- package/server/model/profiles.js +166 -0
- package/server/model/research.js +880 -0
- package/server/model/tf-node.js +26 -0
- package/server/model/transformer-core.js +330 -0
- package/server/model/transformer-gpu.js +221 -0
- package/server/model/transformer-pool.js +200 -0
- package/server/model/transformer-worker.js +28 -0
- package/server/model/transformer.js +57 -0
- package/server/model/vocab.js +28 -0
- package/server/news/index.js +309 -0
- package/server/notify/index.js +168 -0
- package/server/params-meta.js +63 -0
- package/server/settings.js +79 -0
- package/server/strategist/analytics.js +150 -0
- package/server/strategist/index.js +484 -0
- package/server/strategist/tools.js +302 -0
- package/server/util.js +9 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { round } from '../util.js';
|
|
2
|
+
|
|
3
|
+
const MSK = 3 * 3600000;
|
|
4
|
+
|
|
5
|
+
export function computeMetrics(trades, curve, capital) {
|
|
6
|
+
const n = trades.length;
|
|
7
|
+
const wins = trades.filter((t) => t.pnl > 0);
|
|
8
|
+
const losses = trades.filter((t) => t.pnl <= 0);
|
|
9
|
+
const grossProfit = wins.reduce((s, t) => s + t.pnl, 0);
|
|
10
|
+
const grossLoss = -losses.reduce((s, t) => s + t.pnl, 0);
|
|
11
|
+
const net = trades.reduce((s, t) => s + t.pnl, 0);
|
|
12
|
+
|
|
13
|
+
let peak = -Infinity, maxDD = 0;
|
|
14
|
+
for (const p of curve) {
|
|
15
|
+
peak = Math.max(peak, p.equity);
|
|
16
|
+
if (peak > 0) maxDD = Math.max(maxDD, (peak - p.equity) / peak);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Шарп по дневным приращениям капитала, годовой
|
|
20
|
+
const daily = new Map();
|
|
21
|
+
for (const p of curve) daily.set(Math.floor((p.ts + MSK) / 86400000), p.equity);
|
|
22
|
+
const eq = [...daily.values()];
|
|
23
|
+
const rets = [];
|
|
24
|
+
for (let i = 1; i < eq.length; i++) if (eq[i - 1] > 0) rets.push(eq[i] / eq[i - 1] - 1);
|
|
25
|
+
let sharpe = null;
|
|
26
|
+
if (rets.length > 5) {
|
|
27
|
+
const m = rets.reduce((a, b) => a + b, 0) / rets.length;
|
|
28
|
+
const sd = Math.sqrt(rets.reduce((a, b) => a + (b - m) ** 2, 0) / (rets.length - 1));
|
|
29
|
+
sharpe = sd > 0 ? (m / sd) * Math.sqrt(252) : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const start = curve[0]?.equity ?? capital;
|
|
33
|
+
const end = curve.at(-1)?.equity ?? capital;
|
|
34
|
+
return {
|
|
35
|
+
trades: n,
|
|
36
|
+
winRate: n ? round((wins.length / n) * 100, 1) : null,
|
|
37
|
+
profitFactor: grossLoss > 0 ? round(grossProfit / grossLoss, 2) : (grossProfit > 0 ? null : null),
|
|
38
|
+
maxDrawdownPct: round(maxDD * 100, 2),
|
|
39
|
+
sharpe: sharpe === null ? null : round(sharpe, 2),
|
|
40
|
+
netPnl: round(net, 2),
|
|
41
|
+
returnPct: round((end / start - 1) * 100, 2),
|
|
42
|
+
avgTradePct: n ? round(trades.reduce((s, t) => s + t.pnlPct, 0) / n, 3) : null,
|
|
43
|
+
avgWin: wins.length ? round(grossProfit / wins.length, 2) : null,
|
|
44
|
+
avgLoss: losses.length ? round(-grossLoss / losses.length, 2) : null,
|
|
45
|
+
avgBarsHeld: n ? round(trades.reduce((s, t) => s + (t.barsHeld || 0), 0) / n, 1) : null,
|
|
46
|
+
commission: round(trades.reduce((s, t) => s + (t.commission || 0), 0), 2),
|
|
47
|
+
days: eq.length,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Метрики полного периода, а также отдельно in-sample / out-of-sample
|
|
52
|
+
export function computeReport(trades, curve, capital, oosPct) {
|
|
53
|
+
const full = computeMetrics(trades, curve, capital);
|
|
54
|
+
if (!curve.length || !oosPct) return { full };
|
|
55
|
+
const splitTs = curve[Math.floor(curve.length * (1 - oosPct / 100))]?.ts ?? curve.at(-1).ts;
|
|
56
|
+
const isCurve = curve.filter((p) => p.ts < splitTs);
|
|
57
|
+
const oosCurve = curve.filter((p) => p.ts >= splitTs);
|
|
58
|
+
return {
|
|
59
|
+
full,
|
|
60
|
+
inSample: computeMetrics(trades.filter((t) => t.entryTs < splitTs), isCurve, capital),
|
|
61
|
+
outOfSample: computeMetrics(trades.filter((t) => t.entryTs >= splitTs), oosCurve, capital),
|
|
62
|
+
splitTs,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function downsample(curve, max = 1500) {
|
|
67
|
+
if (curve.length <= max) return curve;
|
|
68
|
+
const step = curve.length / max;
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i < max; i++) out.push(curve[Math.floor(i * step)]);
|
|
71
|
+
out.push(curve.at(-1));
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ymOf = (ts) => {
|
|
76
|
+
const d = new Date(ts + MSK);
|
|
77
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
78
|
+
};
|
|
79
|
+
const pct = (a, b) => (b > 0 ? round((a / b - 1) * 100, 2) : null);
|
|
80
|
+
|
|
81
|
+
// Доходность по календарным месяцам и годам (по МСК): конец периода к концу предыдущего, первый — к первой точке кривой
|
|
82
|
+
export function periodReturns(curve, capital) {
|
|
83
|
+
if (!curve?.length) return { months: [], years: [], last12: null };
|
|
84
|
+
const monthEnd = new Map();
|
|
85
|
+
for (const p of curve) monthEnd.set(ymOf(p.ts), p.equity);
|
|
86
|
+
const months = [];
|
|
87
|
+
let prev = curve[0].equity ?? capital;
|
|
88
|
+
for (const [ym, eq] of monthEnd) {
|
|
89
|
+
months.push({ ym, ret: pct(eq, prev), eq });
|
|
90
|
+
prev = eq;
|
|
91
|
+
}
|
|
92
|
+
const yearEnd = new Map(), yearMonths = new Map();
|
|
93
|
+
for (const m of months) {
|
|
94
|
+
const y = +m.ym.slice(0, 4);
|
|
95
|
+
yearEnd.set(y, m.eq);
|
|
96
|
+
yearMonths.set(y, (yearMonths.get(y) || 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
const years = [];
|
|
99
|
+
prev = curve[0].equity ?? capital;
|
|
100
|
+
for (const [y, eq] of yearEnd) {
|
|
101
|
+
years.push({ y, ret: pct(eq, prev), months: yearMonths.get(y), partial: yearMonths.get(y) < 12 });
|
|
102
|
+
prev = eq;
|
|
103
|
+
}
|
|
104
|
+
const last = curve.at(-1);
|
|
105
|
+
const base = [...curve].reverse().find((p) => p.ts <= last.ts - 365 * 86400000) || curve[0];
|
|
106
|
+
return { months: months.map(({ ym, ret }) => ({ ym, ret })), years, last12: pct(last.equity, base.equity) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Сводка для интерфейса: торгующая модель (с комиссиями и без), рынок и остальные модели
|
|
110
|
+
export function buildPeriods({ model, gross, market, models }, capital, splitTs) {
|
|
111
|
+
const pm = periodReturns(model, capital), pg = periodReturns(gross, capital), pk = periodReturns(market, capital);
|
|
112
|
+
const po = Object.fromEntries(Object.entries(models).map(([n, c]) => [n, periodReturns(c, capital)]));
|
|
113
|
+
const find = (arr, key, v) => arr.find((x) => x[key] === v)?.ret ?? null;
|
|
114
|
+
// месяц относится к контрольному периоду, если его середина — после границы (то есть больше половины дней)
|
|
115
|
+
const oosMonth = (ym) => {
|
|
116
|
+
if (!splitTs) return false;
|
|
117
|
+
const [y, m] = ym.split('-').map(Number);
|
|
118
|
+
const a = Date.UTC(y, m - 1, 1) - MSK, b = Date.UTC(y, m, 1) - MSK;
|
|
119
|
+
return splitTs <= (a + b) / 2;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
months: pm.months.map((m) => ({
|
|
123
|
+
ym: m.ym, model: m.ret, gross: find(pg.months, 'ym', m.ym), market: find(pk.months, 'ym', m.ym),
|
|
124
|
+
models: Object.fromEntries(Object.entries(po).map(([n, r]) => [n, find(r.months, 'ym', m.ym)])),
|
|
125
|
+
oos: oosMonth(m.ym),
|
|
126
|
+
})),
|
|
127
|
+
years: pm.years.map((y) => ({
|
|
128
|
+
y: y.y, model: y.ret, gross: find(pg.years, 'y', y.y), market: find(pk.years, 'y', y.y),
|
|
129
|
+
models: Object.fromEntries(Object.entries(po).map(([n, r]) => [n, find(r.years, 'y', y.y)])),
|
|
130
|
+
partial: y.partial, months: y.months,
|
|
131
|
+
})),
|
|
132
|
+
last12: { model: pm.last12, market: pk.last12 },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { eigenSym, ridgeSolve, matrix, spearman, kmeans } from './linalg.js';
|
|
2
|
+
import { BAR_INTERVAL } from './dataset.js';
|
|
3
|
+
|
|
4
|
+
// Масштабы «контекста» в свечах (свеча = 10 минут): последний час, 2 часа до этого, предыдущие 5 часов
|
|
5
|
+
const H1 = 60 / BAR_INTERVAL;
|
|
6
|
+
export const SCALES = [
|
|
7
|
+
{ from: 0, len: H1, label: 'последний час' },
|
|
8
|
+
{ from: H1, len: 2 * H1, label: '2 часа до этого' },
|
|
9
|
+
{ from: 3 * H1, len: 5 * H1, label: 'предыдущие 5 часов' },
|
|
10
|
+
];
|
|
11
|
+
// сколько свечей назад смотрят самые длинные признаки контекста (5 часов до этого, начиная с 3 часов)
|
|
12
|
+
export const FEATURE_WARMUP = (3 + 5) * H1;
|
|
13
|
+
export const KINDS = [{ id: 'price', label: 'движение цены' }, { id: 'flow', label: 'денежный поток (движение × необычность оборота)' }];
|
|
14
|
+
export const BETAS = [{ v: 4, label: 'широкое' }, { v: 12, label: 'узкое' }];
|
|
15
|
+
export const HEADS = [];
|
|
16
|
+
for (const k of KINDS) for (const s of SCALES) for (const b of BETAS) HEADS.push({ kind: k.id, scale: SCALES.indexOf(s), beta: b.v, label: `${k.label}, ${s.label}, ${b.label} внимание` });
|
|
17
|
+
|
|
18
|
+
// ---------- контекст одного шага обучения ----------
|
|
19
|
+
// trainStart..testStart — окно обучения; testStart..testEnd — месяц прогноза.
|
|
20
|
+
// Все массивы «по времени»: [u*N + i], u = t - lo.
|
|
21
|
+
// textWeight (0..1) — вес «текстовой» близости по профилям бизнеса; profiles — Map(тикер → 20 чисел), загружается один раз на исследование.
|
|
22
|
+
export function buildFold(ds, { trainStart, testStart, testEnd, H, embedDim, textWeight = 0, profiles = null }) {
|
|
23
|
+
const { tokens, mret, T } = ds;
|
|
24
|
+
const N = tokens.length;
|
|
25
|
+
const lo = Math.max(1, trainStart - FEATURE_WARMUP);
|
|
26
|
+
const hi = Math.min(T, testEnd + H + 2);
|
|
27
|
+
const W = hi - lo;
|
|
28
|
+
|
|
29
|
+
// чувствительность к рынку и обычный размах — только по окну обучения
|
|
30
|
+
const beta = new Float64Array(N), sigma = new Float64Array(N);
|
|
31
|
+
let mm = 0, mv = 0;
|
|
32
|
+
const n0 = testStart - trainStart;
|
|
33
|
+
for (let t = trainStart; t < testStart; t++) mm += mret[t];
|
|
34
|
+
mm /= n0;
|
|
35
|
+
for (let t = trainStart; t < testStart; t++) mv += (mret[t] - mm) ** 2;
|
|
36
|
+
for (let i = 0; i < N; i++) {
|
|
37
|
+
const r = tokens[i].ret;
|
|
38
|
+
let ri = 0, cov = 0;
|
|
39
|
+
for (let t = trainStart; t < testStart; t++) ri += r[t];
|
|
40
|
+
ri /= n0;
|
|
41
|
+
for (let t = trainStart; t < testStart; t++) cov += (r[t] - ri) * (mret[t] - mm);
|
|
42
|
+
beta[i] = mv > 0 ? cov / mv : 1;
|
|
43
|
+
let s2 = 0;
|
|
44
|
+
for (let t = trainStart; t < testStart; t++) { const e = r[t] - beta[i] * mret[t]; s2 += e * e; }
|
|
45
|
+
sigma[i] = Math.max(Math.sqrt(s2 / n0), 1e-4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// остаточное движение (без общего рынка), нормированное на обычный размах
|
|
49
|
+
const z = new Float64Array(W * N), has = new Uint8Array(W * N), liq = new Uint8Array(W * N), vr = new Float64Array(W * N);
|
|
50
|
+
for (let u = 0; u < W; u++) {
|
|
51
|
+
const t = lo + u;
|
|
52
|
+
for (let i = 0; i < N; i++) {
|
|
53
|
+
const e = tokens[i].ret[t] - beta[i] * mret[t];
|
|
54
|
+
z[u * N + i] = Math.max(-8, Math.min(8, e / sigma[i]));
|
|
55
|
+
has[u * N + i] = tokens[i].has[t];
|
|
56
|
+
liq[u * N + i] = tokens[i].liq[t];
|
|
57
|
+
vr[u * N + i] = Math.min(4, tokens[i].valRatio[t]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// признаки контекста: 2 вида × 3 масштаба (Float32 — признаки не требуют двойной точности, память вдвое меньше)
|
|
61
|
+
const X = KINDS.map(() => SCALES.map(() => new Float32Array(W * N)));
|
|
62
|
+
SCALES.forEach((sc, si) => {
|
|
63
|
+
const Xp = X[0][si], Xf = X[1][si];
|
|
64
|
+
const invP = 1 / Math.sqrt(sc.len), invF = invP / 1.5;
|
|
65
|
+
const sp = new Float64Array(N), sf = new Float64Array(N);
|
|
66
|
+
for (let u = 0; u < W; u++) {
|
|
67
|
+
const a = u - sc.from, b = u - sc.from - sc.len;
|
|
68
|
+
if (a >= 0) {
|
|
69
|
+
const offA = a * N;
|
|
70
|
+
for (let i = 0; i < N; i++) { const zk = z[offA + i]; sp[i] += zk; sf[i] += zk * vr[offA + i]; }
|
|
71
|
+
}
|
|
72
|
+
if (b >= 0) {
|
|
73
|
+
const offB = b * N;
|
|
74
|
+
for (let i = 0; i < N; i++) { const zk = z[offB + i]; sp[i] -= zk; sf[i] -= zk * vr[offB + i]; }
|
|
75
|
+
}
|
|
76
|
+
if (u >= FEATURE_WARMUP) {
|
|
77
|
+
const offU = u * N;
|
|
78
|
+
for (let i = 0; i < N; i++) { Xp[offU + i] = sp[i] * invP; Xf[offU + i] = sf[i] * invF; }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// цель: остаточное (без рынка) движение, которое реально можно получить: решение на закрытии свечи t,
|
|
83
|
+
// вход по цене открытия t+1, выход по цене открытия t+1+H
|
|
84
|
+
const { mopen } = ds;
|
|
85
|
+
const yz = new Float64Array(W * N), yraw = new Float64Array(W * N), yok = new Uint8Array(W);
|
|
86
|
+
for (let u = 0; u + 1 + H < W; u++) {
|
|
87
|
+
const t = lo + u;
|
|
88
|
+
const ma = mopen[t + 1], mb = mopen[t + 1 + H];
|
|
89
|
+
if (!(ma > 0 && mb > 0)) continue;
|
|
90
|
+
yok[u] = 1;
|
|
91
|
+
const m = Math.log(mb / ma);
|
|
92
|
+
for (let i = 0; i < N; i++) {
|
|
93
|
+
const a = tokens[i].open[t + 1], b = tokens[i].open[t + 1 + H];
|
|
94
|
+
const r = a > 0 && b > 0 ? Math.log(b / a) - beta[i] * m : 0;
|
|
95
|
+
yraw[u * N + i] = r;
|
|
96
|
+
yz[u * N + i] = Math.max(-8, Math.min(8, r / (sigma[i] * Math.sqrt(H))));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------- карта рынка: эмбеддинги из матрицы совместных движений ----------
|
|
101
|
+
const C = matrix(N, N);
|
|
102
|
+
const u0 = trainStart - lo, u1 = testStart - lo;
|
|
103
|
+
for (let u = u0; u < u1; u++) {
|
|
104
|
+
const off = u * N;
|
|
105
|
+
for (let i = 0; i < N; i++) {
|
|
106
|
+
const zi = z[off + i];
|
|
107
|
+
if (zi === 0) continue;
|
|
108
|
+
const Ci = C[i];
|
|
109
|
+
for (let j = i; j < N; j++) Ci[j] += zi * z[off + j];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (let i = 0; i < N; i++) for (let j = i; j < N; j++) { C[i][j] /= (u1 - u0); C[j][i] = C[i][j]; }
|
|
113
|
+
const diag = Array.from({ length: N }, (_, i) => Math.sqrt(Math.max(C[i][i], 1e-9)));
|
|
114
|
+
for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) C[i][j] /= diag[i] * diag[j];
|
|
115
|
+
const eig = eigenSym(C, { maxSweeps: 30 });
|
|
116
|
+
const d = Math.min(embedDim, N - 1);
|
|
117
|
+
const emb = Array.from({ length: N }, (_, i) => Float64Array.from({ length: d }, (_, c) => eig.vectors[c][i] * Math.sqrt(Math.max(eig.values[c], 0))));
|
|
118
|
+
const embN = emb.map((e) => { const n = Math.hypot(...e) || 1; return e.map((v) => v / n); });
|
|
119
|
+
const cos = matrix(N, N);
|
|
120
|
+
for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) { let s = 0; for (let c = 0; c < d; c++) s += embN[i][c] * embN[j][c]; cos[i][j] = s; }
|
|
121
|
+
let eigVals = eig.values.slice(0, d);
|
|
122
|
+
|
|
123
|
+
// ---------- смешивание с «текстовой» близостью (устойчивые признаки бизнеса из профилей ИИ) ----------
|
|
124
|
+
// итог: cos = (1−w)·близость по поведению + w·близость по центрированным профилям;
|
|
125
|
+
// у пары акций без профиля — только поведенческая близость. Эмбеддинги карты пересчитываются из смешанной матрицы.
|
|
126
|
+
const tw = Math.max(0, Math.min(1, Number(textWeight) || 0));
|
|
127
|
+
if (tw > 0 && profiles?.size) {
|
|
128
|
+
const pv = tokens.map((t) => { const v = profiles.get(t.symbol); return Array.isArray(v) && v.every((x) => Number.isFinite(x)) ? v : null; });
|
|
129
|
+
const L = pv.find(Boolean)?.length || 0;
|
|
130
|
+
const have = pv.filter(Boolean).length;
|
|
131
|
+
if (L >= 2 && have >= 2) {
|
|
132
|
+
// центрируем профили: вычитаем среднее по признаку, иначе все компании «похожи» из-за общих единиц шкалы
|
|
133
|
+
const mean = new Float64Array(L);
|
|
134
|
+
for (const v of pv) if (v) for (let f = 0; f < L; f++) mean[f] += v[f] ?? 0;
|
|
135
|
+
for (let f = 0; f < L; f++) mean[f] /= have;
|
|
136
|
+
const cent = pv.map((v) => (v ? Float64Array.from({ length: L }, (_, f) => (v[f] ?? 0) - mean[f]) : null));
|
|
137
|
+
const nrm = cent.map((c) => (c ? Math.sqrt(c.reduce((s, x) => s + x * x, 0)) : 0));
|
|
138
|
+
for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) {
|
|
139
|
+
let t = null;
|
|
140
|
+
if (cent[i] && cent[j] && nrm[i] > 0 && nrm[j] > 0) {
|
|
141
|
+
let s = 0; for (let f = 0; f < L; f++) s += cent[i][f] * cent[j][f];
|
|
142
|
+
t = s / (nrm[i] * nrm[j]);
|
|
143
|
+
}
|
|
144
|
+
const beh = cos[i][j];
|
|
145
|
+
cos[i][j] = (1 - tw) * beh + tw * (t === null ? beh : t);
|
|
146
|
+
}
|
|
147
|
+
// эмбеддинги и «головы внимания» дальше используют смешанную матрицу
|
|
148
|
+
const em = eigenSym(cos, { maxSweeps: 30 });
|
|
149
|
+
for (let i = 0; i < N; i++) {
|
|
150
|
+
const e = Float64Array.from({ length: d }, (_, c) => em.vectors[c][i] * Math.sqrt(Math.max(em.values[c], 0)));
|
|
151
|
+
const n = Math.hypot(...e) || 1;
|
|
152
|
+
emb[i] = e;
|
|
153
|
+
embN[i] = e.map((v) => v / n);
|
|
154
|
+
}
|
|
155
|
+
eigVals = em.values.slice(0, d);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------- макро-головы: чувствительность остатков акций к макро-инструментам (только окно обучения) ----------
|
|
160
|
+
let macro = null;
|
|
161
|
+
if (ds.macro?.length) {
|
|
162
|
+
const list = [];
|
|
163
|
+
for (const m of ds.macro) {
|
|
164
|
+
// размах макро — только по торгованным барам окна обучения (у валют/фьючерсов могут быть пустоты)
|
|
165
|
+
let cnt = 0, mm = 0;
|
|
166
|
+
for (let t = trainStart; t < testStart; t++) if (m.has[t]) { mm += m.ret[t]; cnt++; }
|
|
167
|
+
if (cnt < n0 * 0.2) continue; // макро почти не торговался в окне — пропускаем
|
|
168
|
+
mm /= cnt;
|
|
169
|
+
let mv = 0;
|
|
170
|
+
for (let t = trainStart; t < testStart; t++) if (m.has[t]) mv += (m.ret[t] - mm) ** 2;
|
|
171
|
+
const msig = Math.max(Math.sqrt(mv / Math.max(cnt, 1)), 1e-6);
|
|
172
|
+
// бета остаточного движения акции к доходности макро
|
|
173
|
+
const b = new Float64Array(N);
|
|
174
|
+
for (let i = 0; i < N; i++) {
|
|
175
|
+
const r = tokens[i].ret;
|
|
176
|
+
let cov = 0;
|
|
177
|
+
for (let t = trainStart; t < testStart; t++) if (m.has[t]) cov += (r[t] - beta[i] * mret[t]) * (m.ret[t] - mm);
|
|
178
|
+
b[i] = mv > 0 ? cov / mv : 0;
|
|
179
|
+
}
|
|
180
|
+
// z-движения макро и их масштабные суммы (как признаки контекста у акций)
|
|
181
|
+
const z = new Float32Array(W);
|
|
182
|
+
for (let u = 0; u < W; u++) { const v = m.ret[lo + u] / msig; z[u] = Math.max(-8, Math.min(8, v)); }
|
|
183
|
+
const zs = new Float32Array(SCALES.length * W);
|
|
184
|
+
SCALES.forEach((sc, si) => {
|
|
185
|
+
const out = zs.subarray(si * W, (si + 1) * W);
|
|
186
|
+
let sum = 0;
|
|
187
|
+
for (let u = 0; u < W; u++) {
|
|
188
|
+
const a = u - sc.from, bb = u - sc.from - sc.len;
|
|
189
|
+
if (a >= 0) sum += z[a];
|
|
190
|
+
if (bb >= 0) sum -= z[bb];
|
|
191
|
+
out[u] = a >= 0 ? sum / Math.sqrt(sc.len) : 0;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
list.push({ id: m.id, label: m.label, b, zs });
|
|
195
|
+
}
|
|
196
|
+
if (list.length) macro = { list };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { N, lo, hi, W, trainStart, testStart, testEnd, H, beta, sigma, z, has, liq, X, yz, yraw, yok, emb, embN, cos, eigenvalues: eigVals, macro };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Ликвидная акция: сделки хотя бы в 15 из последних 20 свечей
|
|
203
|
+
export const LIQ_MIN = 15;
|
|
204
|
+
|
|
205
|
+
// В обучение — только моменты, цель которых целиком лежит в окне обучения (без заглядывания в месяц прогноза)
|
|
206
|
+
export const trainUs = (f) => {
|
|
207
|
+
const us = [];
|
|
208
|
+
for (let t = f.trainStart + FEATURE_WARMUP; t < f.testStart - f.H - 1; t++) { const u = t - f.lo; if (f.yok[u]) us.push(u); }
|
|
209
|
+
return us;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Сводная (общая для всех акций) гребневая регрессия: y ~ features
|
|
213
|
+
function pooledFit(f, featFn, p, lambdaRel = 1e-3) {
|
|
214
|
+
const A = matrix(p, p), b = matrix(p, 1);
|
|
215
|
+
const x = new Float64Array(p);
|
|
216
|
+
for (const u of trainUs(f)) {
|
|
217
|
+
for (let i = 0; i < f.N; i++) {
|
|
218
|
+
if (!f.has[u * f.N + i] || f.liq[u * f.N + i] < LIQ_MIN) continue;
|
|
219
|
+
featFn(u, i, x);
|
|
220
|
+
const y = f.yz[u * f.N + i];
|
|
221
|
+
for (let a = 0; a < p; a++) {
|
|
222
|
+
const xa = x[a];
|
|
223
|
+
if (xa === 0) continue;
|
|
224
|
+
b[a][0] += xa * y;
|
|
225
|
+
const Aa = A[a];
|
|
226
|
+
for (let c = a; c < p; c++) Aa[c] += xa * x[c];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let tr = 0;
|
|
231
|
+
for (let a = 0; a < p; a++) { for (let c = 0; c < a; c++) A[a][c] = A[c][a]; tr += A[a][a]; }
|
|
232
|
+
return ridgeSolve(A, b, (tr / p) * lambdaRel).map((r) => r[0]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------- ступень 0: своя инерция ----------
|
|
236
|
+
export function fitInertia(f) {
|
|
237
|
+
const coef = pooledFit(f, (u, i, x) => { for (let s = 0; s < 3; s++) x[s] = f.X[0][s][u * f.N + i]; }, 3);
|
|
238
|
+
return { name: 'inertia', coef };
|
|
239
|
+
}
|
|
240
|
+
function predictInertia(m, f, u) {
|
|
241
|
+
const out = new Float64Array(f.N);
|
|
242
|
+
for (let i = 0; i < f.N; i++) { let s = 0; for (let k = 0; k < 3; k++) s += m.coef[k] * f.X[0][k][u * f.N + i]; out[i] = s; }
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------- ступень 1: кто за кем ходит (линейное «внимание» без карты) ----------
|
|
247
|
+
export function fitLeadLag(f) {
|
|
248
|
+
const N = f.N, p = 2 * N;
|
|
249
|
+
const us = trainUs(f);
|
|
250
|
+
const split = Math.floor(us.length * 0.8);
|
|
251
|
+
const accum = (list) => {
|
|
252
|
+
const A = matrix(p, p), B = matrix(p, N);
|
|
253
|
+
const x = new Float64Array(p);
|
|
254
|
+
for (const u of list) {
|
|
255
|
+
for (let i = 0; i < N; i++) { x[i] = f.X[0][0][u * N + i]; x[N + i] = f.X[0][1][u * N + i]; }
|
|
256
|
+
for (let a = 0; a < p; a++) {
|
|
257
|
+
const xa = x[a];
|
|
258
|
+
if (xa === 0) continue;
|
|
259
|
+
const Aa = A[a], Ba = B[a];
|
|
260
|
+
for (let c = a; c < p; c++) Aa[c] += xa * x[c];
|
|
261
|
+
for (let j = 0; j < N; j++) Ba[j] += xa * f.yz[u * N + j];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (let a = 0; a < p; a++) for (let c = 0; c < a; c++) A[a][c] = A[c][a];
|
|
265
|
+
return { A, B };
|
|
266
|
+
};
|
|
267
|
+
const P1 = accum(us.slice(0, split)), P2 = accum(us.slice(split));
|
|
268
|
+
const trace = (A) => A.reduce((s, r, i) => s + r[i], 0) / p;
|
|
269
|
+
// выбор силы регуляризации на последних 20% окна обучения
|
|
270
|
+
let best = null;
|
|
271
|
+
for (const alpha of [3, 30, 300]) {
|
|
272
|
+
const Wt = ridgeSolve(P1.A, P1.B, trace(P1.A) * alpha);
|
|
273
|
+
let ic = 0, n = 0;
|
|
274
|
+
for (const u of us.slice(split)) {
|
|
275
|
+
const pred = predictLin(Wt, f, u);
|
|
276
|
+
ic += spearman(pred, f.yz.subarray(u * N, u * N + N)); n++;
|
|
277
|
+
}
|
|
278
|
+
ic /= n || 1;
|
|
279
|
+
if (!best || ic > best.ic) best = { alpha, ic };
|
|
280
|
+
}
|
|
281
|
+
const A = P1.A.map((r, a) => r.map((v, c) => v + P2.A[a][c]));
|
|
282
|
+
const B = P1.B.map((r, a) => r.map((v, c) => v + P2.B[a][c]));
|
|
283
|
+
return { name: 'leadlag', W: ridgeSolve(A, B, trace(A) * best.alpha), alpha: best.alpha };
|
|
284
|
+
}
|
|
285
|
+
function predictLin(Wt, f, u) {
|
|
286
|
+
const N = f.N, out = new Float64Array(N);
|
|
287
|
+
for (let a = 0; a < 2 * N; a++) {
|
|
288
|
+
const xa = a < N ? f.X[0][0][u * N + a] : f.X[0][1][u * N + a - N];
|
|
289
|
+
if (xa === 0) continue;
|
|
290
|
+
const Wa = Wt[a];
|
|
291
|
+
for (let j = 0; j < N; j++) out[j] += xa * Wa[j];
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------- ступень 2: внимание через карту рынка ----------
|
|
297
|
+
// Голова h: влияние соседей i на акцию j с весом softmax(beta·близость), сама акция исключена
|
|
298
|
+
export function attentionMatrices(f) {
|
|
299
|
+
const N = f.N;
|
|
300
|
+
return BETAS.map((b) => {
|
|
301
|
+
const A = new Float64Array(N * N);
|
|
302
|
+
for (let j = 0; j < N; j++) {
|
|
303
|
+
let s = 0;
|
|
304
|
+
for (let i = 0; i < N; i++) { if (i === j) continue; const w = Math.exp(b.v * f.cos[j][i]); A[j * N + i] = w; s += w; }
|
|
305
|
+
for (let i = 0; i < N; i++) A[j * N + i] /= s;
|
|
306
|
+
}
|
|
307
|
+
return A;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function headFeatures(f, As) {
|
|
312
|
+
const { N, W } = f;
|
|
313
|
+
const F = HEADS.map(() => new Float32Array(W * N));
|
|
314
|
+
for (let bIdx = 0; bIdx < BETAS.length; bIdx++) {
|
|
315
|
+
const betaVal = BETAS[bIdx].v;
|
|
316
|
+
const A = As[bIdx];
|
|
317
|
+
const hIdxs = [];
|
|
318
|
+
for (let hi = 0; hi < HEADS.length; hi++) if (HEADS[hi].beta === betaVal) hIdxs.push(hi);
|
|
319
|
+
const Xs = hIdxs.map((hi) => f.X[HEADS[hi].kind === 'price' ? 0 : 1][HEADS[hi].scale]);
|
|
320
|
+
const Fs = hIdxs.map((hi) => F[hi]);
|
|
321
|
+
const [X0, X1, X2, X3, X4, X5] = Xs;
|
|
322
|
+
const [F0, F1, F2, F3, F4, F5] = Fs;
|
|
323
|
+
for (let u = FEATURE_WARMUP; u < W; u++) {
|
|
324
|
+
const off = u * N;
|
|
325
|
+
for (let j = 0; j < N; j++) {
|
|
326
|
+
let s0 = 0, s1 = 0, s2 = 0, s3 = 0, s4 = 0, s5 = 0;
|
|
327
|
+
const row = j * N;
|
|
328
|
+
for (let i = 0; i < N; i++) {
|
|
329
|
+
const w = A[row + i];
|
|
330
|
+
const k = off + i;
|
|
331
|
+
s0 += w * X0[k]; s1 += w * X1[k]; s2 += w * X2[k];
|
|
332
|
+
s3 += w * X3[k]; s4 += w * X4[k]; s5 += w * X5[k];
|
|
333
|
+
}
|
|
334
|
+
const kj = off + j;
|
|
335
|
+
F0[kj] = s0; F1[kj] = s1; F2[kj] = s2;
|
|
336
|
+
F3[kj] = s3; F4[kj] = s4; F5[kj] = s5;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return F;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function fitAttention(f) {
|
|
344
|
+
const As = attentionMatrices(f);
|
|
345
|
+
const F = headFeatures(f, As);
|
|
346
|
+
const ML = f.macro ? f.macro.list : [];
|
|
347
|
+
const Hn = HEADS.length;
|
|
348
|
+
const p = Hn + 3 + ML.length * SCALES.length;
|
|
349
|
+
const coef = pooledFit(f, (u, i, x) => {
|
|
350
|
+
const k = u * f.N + i;
|
|
351
|
+
for (let h = 0; h < Hn; h++) x[h] = F[h][k];
|
|
352
|
+
x[Hn] = f.X[0][0][k];
|
|
353
|
+
x[Hn + 1] = f.X[0][1][k];
|
|
354
|
+
x[Hn + 2] = f.X[0][2][k];
|
|
355
|
+
// макро-головы: бета акции к макро × z-движение макро на масштабе
|
|
356
|
+
for (let m = 0; m < ML.length; m++) {
|
|
357
|
+
const b = ML[m].b[i], zs = ML[m].zs, base = Hn + 3 + m * 3;
|
|
358
|
+
x[base] = b * zs[u];
|
|
359
|
+
x[base + 1] = b * zs[f.W + u];
|
|
360
|
+
x[base + 2] = b * zs[2 * f.W + u];
|
|
361
|
+
}
|
|
362
|
+
}, p, 1e-2);
|
|
363
|
+
const macroHeads = ML.flatMap((m, mi) => SCALES.map((sc, si) => ({ label: `${m.label}, ${sc.label}`, coef: coef[Hn + 3 + mi * 3 + si] })));
|
|
364
|
+
return { name: 'attention', coef, As, F, macroHeads };
|
|
365
|
+
}
|
|
366
|
+
function predictAttention(m, f, u) {
|
|
367
|
+
const ML = f.macro ? f.macro.list : [];
|
|
368
|
+
const out = new Float64Array(f.N);
|
|
369
|
+
for (let i = 0; i < f.N; i++) {
|
|
370
|
+
const k = u * f.N + i;
|
|
371
|
+
let s = 0;
|
|
372
|
+
for (let h = 0; h < HEADS.length; h++) s += m.coef[h] * m.F[h][k];
|
|
373
|
+
for (let q = 0; q < 3; q++) s += m.coef[HEADS.length + q] * f.X[0][q][k];
|
|
374
|
+
for (let mi = 0; mi < ML.length; mi++) {
|
|
375
|
+
let zz = 0;
|
|
376
|
+
const zs = ML[mi].zs;
|
|
377
|
+
for (let q = 0; q < 3; q++) zz += m.coef[HEADS.length + 3 + mi * 3 + q] * zs[q * f.W + u];
|
|
378
|
+
s += ML[mi].b[i] * zz;
|
|
379
|
+
}
|
|
380
|
+
out[i] = s;
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const MODELS = {
|
|
386
|
+
inertia: { label: 'Своя инерция', fit: fitInertia, predict: predictInertia },
|
|
387
|
+
leadlag: { label: 'Кто за кем ходит', fit: fitLeadLag, predict: (m, f, u) => predictLin(m.W, f, u) },
|
|
388
|
+
attention: { label: 'Внимание через карту', fit: fitAttention, predict: predictAttention },
|
|
389
|
+
// ступень 3: нейросеть с вниманием между акциями (tfjs грузится только при первом обучении);
|
|
390
|
+
// fit асинхронный, после использования модель нужно освободить: model.dispose()
|
|
391
|
+
transformer: {
|
|
392
|
+
label: 'Трансформер',
|
|
393
|
+
fit: async (f, opts) => (await import('./transformer.js')).fitTransformer(f, opts),
|
|
394
|
+
predict: (m, f, u) => m.predictOne(m, f, u),
|
|
395
|
+
predictMany: (m, f, us) => m.predictMany(m, f, us),
|
|
396
|
+
heavy: true,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Какие модели обучать: три базовые всегда, трансформер — если включён для сравнения или торгует
|
|
401
|
+
export const modelNames = (p) => Object.keys(MODELS).filter((n) => !MODELS[n].heavy || (n === 'transformer' && (p.modelUseTransformer || p.modelType === 'transformer')));
|
|
402
|
+
|
|
403
|
+
// Объяснение прогноза «внимания»: вклад каждого соседа в прогноз акции j
|
|
404
|
+
export function explainAttention(m, f, u, j, top = 3) {
|
|
405
|
+
const N = f.N;
|
|
406
|
+
const contrib = new Float64Array(N);
|
|
407
|
+
HEADS.forEach((h, hi) => {
|
|
408
|
+
const A = m.As[BETAS.findIndex((b) => b.v === h.beta)];
|
|
409
|
+
const X = f.X[h.kind === 'price' ? 0 : 1][h.scale];
|
|
410
|
+
for (let i = 0; i < N; i++) contrib[i] += m.coef[hi] * A[j * N + i] * X[u * N + i];
|
|
411
|
+
});
|
|
412
|
+
let self = 0;
|
|
413
|
+
for (let q = 0; q < 3; q++) self += m.coef[HEADS.length + q] * f.X[0][q][u * N + j];
|
|
414
|
+
const idx = [...contrib.keys()].filter((i) => i !== j).sort((a, b) => Math.abs(contrib[b]) - Math.abs(contrib[a])).slice(0, top);
|
|
415
|
+
return { neighbors: idx.map((i) => ({ i, contrib: contrib[i] })), self };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 2D-координаты карты + кластеры
|
|
419
|
+
export function mapSnapshot(f, k = 8) {
|
|
420
|
+
const pts = f.emb.map((e) => [e[0] || 0, e[1] || 0]);
|
|
421
|
+
const clusters = kmeans(f.embN.map((e) => Array.from(e)), Math.min(k, f.N), { seed: 11 });
|
|
422
|
+
return { pts, clusters };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Совмещение карты с предыдущей (поворот/отражение), чтобы точки не «прыгали» между месяцами
|
|
426
|
+
export function alignTo(prev, cur) {
|
|
427
|
+
if (!prev) return cur;
|
|
428
|
+
let best = null;
|
|
429
|
+
for (const flip of [1, -1]) {
|
|
430
|
+
let a = 0, b = 0;
|
|
431
|
+
for (let i = 0; i < cur.length; i++) {
|
|
432
|
+
const [x, y] = [cur[i][0], cur[i][1] * flip];
|
|
433
|
+
const [px, py] = prev[i];
|
|
434
|
+
a += x * px + y * py;
|
|
435
|
+
b += x * py - y * px;
|
|
436
|
+
}
|
|
437
|
+
const th = Math.atan2(b, a), c = Math.cos(th), s = Math.sin(th);
|
|
438
|
+
const out = cur.map(([x, y0]) => { const y = y0 * flip; return [c * x - s * y, s * x + c * y]; });
|
|
439
|
+
let err = 0;
|
|
440
|
+
for (let i = 0; i < out.length; i++) err += (out[i][0] - prev[i][0]) ** 2 + (out[i][1] - prev[i][1]) ** 2;
|
|
441
|
+
if (!best || err < best.err) best = { err, out };
|
|
442
|
+
}
|
|
443
|
+
return best.out;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Перенумерация кластеров, чтобы цвета групп сохранялись между месяцами
|
|
447
|
+
export function matchClusters(prev, cur, k) {
|
|
448
|
+
if (!prev) return cur;
|
|
449
|
+
const map = new Map(), used = new Set();
|
|
450
|
+
const pairs = [];
|
|
451
|
+
for (let a = 0; a < k; a++) for (let b = 0; b < k; b++) {
|
|
452
|
+
let n = 0;
|
|
453
|
+
for (let i = 0; i < cur.length; i++) if (cur[i] === a && prev[i] === b) n++;
|
|
454
|
+
pairs.push([n, a, b]);
|
|
455
|
+
}
|
|
456
|
+
pairs.sort((x, y) => y[0] - x[0]);
|
|
457
|
+
for (const [, a, b] of pairs) if (!map.has(a) && !used.has(b)) { map.set(a, b); used.add(b); }
|
|
458
|
+
return cur.map((c) => map.get(c) ?? c);
|
|
459
|
+
}
|