neuroalpha 0.3.13 → 0.3.15
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 +1 -1
- package/docs//320/221/320/240/320/236/320/232/320/225/320/240/320/253_API.md +118 -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/package.json +1 -1
- package/public/app.js +434 -49
- package/public/index.html +22 -8
- package/public/styles.css +4 -2
- package/server/api/routes.js +1 -1
- package/server/brokers/index.js +388 -45
- package/server/model/forecast.js +176 -18
- package/server/model/live.js +31 -7
- package/server/model/research.js +3 -3
package/server/model/forecast.js
CHANGED
|
@@ -1,21 +1,127 @@
|
|
|
1
1
|
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
2
|
import { normalizeParams } from '../settings.js';
|
|
3
|
-
import { round, DAY, fmtBarMsk } from '../util.js';
|
|
3
|
+
import { round, DAY, MSK, fmtBarMsk } from '../util.js';
|
|
4
4
|
import { loadDataset, barsFromHours, BAR_MS } from './dataset.js';
|
|
5
|
-
import { buildFold, MODELS, HEADS, SCALES, BETAS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
|
|
5
|
+
import { buildFold, MODELS, HEADS, KINDS, SCALES, BETAS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
|
|
6
6
|
import { loadProfiles } from './profiles.js';
|
|
7
7
|
import { isMarketDown } from './research.js';
|
|
8
8
|
import { newsForSymbols } from '../news/index.js';
|
|
9
9
|
|
|
10
|
+
const trainedModelCache = new Map(); // fitKey -> { dayMsk, trainedAt, fTrain, fitted, snap }
|
|
10
11
|
let baseState = null; // { key, trainedAt, ds, last, r, snap, prevExpMap, prevRankMap, lastTickResult, lastTickAt }
|
|
11
12
|
const inflight = new Map();
|
|
12
13
|
|
|
14
|
+
// Быстрый расчёт признаков контекста в одной точке idx без переобучения матрицы внимания и весов (за ~1-2 мс)
|
|
15
|
+
function buildInferenceFoldAt(ds, idx, fTrain, fitted, liveSnap = null) {
|
|
16
|
+
const { tokens, mret, mclose } = ds;
|
|
17
|
+
const N = fTrain.N;
|
|
18
|
+
const { beta, sigma, cos } = fTrain;
|
|
19
|
+
const lo = Math.max(1, idx - FEATURE_WARMUP - 2);
|
|
20
|
+
const u = idx - lo;
|
|
21
|
+
const W = u + 1;
|
|
22
|
+
|
|
23
|
+
const z = new Float64Array(W * N);
|
|
24
|
+
const vr = new Float64Array(W * N);
|
|
25
|
+
for (let uk = 0; uk < W; uk++) {
|
|
26
|
+
const t = lo + uk;
|
|
27
|
+
const isLast = uk === u && liveSnap?.prices?.size > 0;
|
|
28
|
+
const mPrev = mclose?.[t - 1] || 0;
|
|
29
|
+
const mt = isLast && liveSnap.imoexPrice > 0 && mPrev > 0 ? Math.log(liveSnap.imoexPrice / mPrev) : mret[t];
|
|
30
|
+
for (let i = 0; i < N; i++) {
|
|
31
|
+
const tk = tokens[i];
|
|
32
|
+
let rt = tk.ret[t];
|
|
33
|
+
if (isLast) {
|
|
34
|
+
const prevPx = tk.close[t - 1];
|
|
35
|
+
const livePx = liveSnap.prices.get(tk.symbol);
|
|
36
|
+
if (livePx > 0 && prevPx > 0) rt = Math.log(livePx / prevPx);
|
|
37
|
+
}
|
|
38
|
+
const e = rt - beta[i] * mt;
|
|
39
|
+
z[uk * N + i] = Math.max(-8, Math.min(8, e / sigma[i]));
|
|
40
|
+
vr[uk * N + i] = Math.min(4, tk.valRatio[t]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const X = KINDS.map(() => SCALES.map(() => new Float32Array(W * N)));
|
|
45
|
+
SCALES.forEach((sc, si) => {
|
|
46
|
+
const Xp = X[0][si], Xf = X[1][si];
|
|
47
|
+
const invP = 1 / Math.sqrt(sc.len), invF = invP / 1.5;
|
|
48
|
+
for (let i = 0; i < N; i++) {
|
|
49
|
+
let sp = 0, sf = 0;
|
|
50
|
+
for (let k = 0; k < sc.len; k++) {
|
|
51
|
+
const uk = u - sc.from - k;
|
|
52
|
+
if (uk < 0) break;
|
|
53
|
+
const zk = z[uk * N + i];
|
|
54
|
+
sp += zk;
|
|
55
|
+
sf += zk * vr[uk * N + i];
|
|
56
|
+
}
|
|
57
|
+
Xp[u * N + i] = sp * invP;
|
|
58
|
+
Xf[u * N + i] = sf * invF;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let macro = null;
|
|
63
|
+
if (fTrain.macro?.list?.length && ds.macro?.length) {
|
|
64
|
+
const list = fTrain.macro.list.map((mTrain) => {
|
|
65
|
+
const dm = ds.macro.find((x) => x.id === mTrain.id);
|
|
66
|
+
const zs = new Float32Array(SCALES.length * W);
|
|
67
|
+
if (dm) {
|
|
68
|
+
const msig = mTrain.msig || 1e-3;
|
|
69
|
+
SCALES.forEach((sc, si) => {
|
|
70
|
+
let sum = 0;
|
|
71
|
+
for (let k = 0; k < sc.len; k++) {
|
|
72
|
+
const t = idx - sc.from - k;
|
|
73
|
+
if (t >= 0) sum += Math.max(-8, Math.min(8, dm.ret[t] / msig));
|
|
74
|
+
}
|
|
75
|
+
zs[si * W + u] = sum / Math.sqrt(sc.len);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return { id: mTrain.id, label: mTrain.label, b: mTrain.b, zs };
|
|
79
|
+
});
|
|
80
|
+
macro = { list };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const f = { N, lo, hi: idx + 1, W, beta, sigma, z, X, cos, emb: fTrain.emb, embN: fTrain.embN, macro };
|
|
84
|
+
const att = fitted.attention;
|
|
85
|
+
let fittedForStep = fitted;
|
|
86
|
+
if (att?.As) {
|
|
87
|
+
const F = HEADS.map(() => new Float32Array(W * N));
|
|
88
|
+
const off = u * N;
|
|
89
|
+
for (let hi = 0; hi < HEADS.length; hi++) {
|
|
90
|
+
const h = HEADS[hi];
|
|
91
|
+
const bIdx = BETAS.findIndex((b) => b.v === h.beta);
|
|
92
|
+
const A = att.As[bIdx];
|
|
93
|
+
const Xh = X[h.kind === 'price' ? 0 : 1][h.scale];
|
|
94
|
+
const Fh = F[hi];
|
|
95
|
+
for (let j = 0; j < N; j++) {
|
|
96
|
+
let s = 0;
|
|
97
|
+
const row = j * N;
|
|
98
|
+
for (let i = 0; i < N; i++) s += A[row + i] * Xh[off + i];
|
|
99
|
+
Fh[off + j] = s;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fittedForStep = { ...fitted, attention: { ...att, F } };
|
|
103
|
+
}
|
|
104
|
+
return { f, u, fitted: fittedForStep };
|
|
105
|
+
}
|
|
106
|
+
|
|
13
107
|
export const lastClosedIndex = (ds, now = Date.now()) => {
|
|
14
108
|
let last = ds.T - 1;
|
|
15
109
|
while (last > 0 && ds.timeline[last] + BAR_MS > now) last--;
|
|
16
110
|
return last;
|
|
17
111
|
};
|
|
18
112
|
|
|
113
|
+
export const isModelTrainedToday = (p, now = Date.now()) => {
|
|
114
|
+
const autoAdapt = p.modelAutoAdapt !== false;
|
|
115
|
+
const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
|
|
116
|
+
const chosen = p.modelType in MODELS ? p.modelType : 'attention';
|
|
117
|
+
const todayMsk = new Date(now + MSK).toISOString().slice(0, 10);
|
|
118
|
+
const prefix = JSON.stringify([autoAdapt, chosen, effHorizon, p.modelTrainDays, p.modelEmbedDim, p.modelUseMacro, p.modelUseDividends, p.modelTextWeight]).slice(0, -1);
|
|
119
|
+
for (const [k, v] of trainedModelCache) {
|
|
120
|
+
if (v.dayMsk === todayMsk && k.startsWith(prefix)) return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
19
125
|
// Быстрое получение живых цен Мосбиржи (TQBR + IMOEX) за 1 лёгкий запрос (~80 мс)
|
|
20
126
|
async function fetchLiveMoexSnapshot() {
|
|
21
127
|
try {
|
|
@@ -107,28 +213,63 @@ function applyLivePricesToFold(ds, f, u, fitted, liveSnap) {
|
|
|
107
213
|
}
|
|
108
214
|
}
|
|
109
215
|
|
|
110
|
-
// Обучение
|
|
111
|
-
//
|
|
112
|
-
export async function predictAt(ds, p, idx, { explain = true, liveSnap = null } = {}) {
|
|
216
|
+
// Обучение ядра модели (карта рынка + веса внимания) выполняется не чаще 1 раза в торговый день для каждого набора параметров.
|
|
217
|
+
// На всех свечах внутри дня выполняется быстрый расчёт прогноза по уже обученным весам (buildInferenceFoldAt).
|
|
218
|
+
export async function predictAt(ds, p, idx, { explain = true, liveSnap = null, forceTrain = false } = {}) {
|
|
113
219
|
const autoAdapt = p.modelAutoAdapt !== false;
|
|
114
220
|
const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
|
|
115
221
|
const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
|
|
116
222
|
const H = barsFromHours(effHorizon);
|
|
117
223
|
const maxH = autoAdapt ? Math.round(H * 1.4) : H;
|
|
224
|
+
const chosen = p.modelType in MODELS ? p.modelType : 'attention';
|
|
225
|
+
const fitKey = JSON.stringify([autoAdapt, chosen, effHorizon, p.modelTrainDays, p.modelEmbedDim, p.modelUseMacro, p.modelUseDividends, p.modelTextWeight, ds.tokens.length]);
|
|
226
|
+
const barDayMsk = new Date((ds.timeline[idx] || Date.now()) + MSK).toISOString().slice(0, 10);
|
|
227
|
+
|
|
228
|
+
const cached = trainedModelCache.get(fitKey);
|
|
229
|
+
if (!forceTrain && cached && cached.dayMsk === barDayMsk && cached.fTrain.N === ds.tokens.length) {
|
|
230
|
+
const inf = buildInferenceFoldAt(ds, idx, cached.fTrain, cached.fitted, liveSnap);
|
|
231
|
+
return {
|
|
232
|
+
...buildPredictionRows(ds, p, idx, inf.f, inf.u, inf.fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap),
|
|
233
|
+
snap: cached.snap,
|
|
234
|
+
trainedAt: cached.trainedAt,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
118
238
|
const trainLen = Math.round(p.modelTrainDays * ds.barsPerDay);
|
|
119
239
|
const trainStart = Math.max(FEATURE_WARMUP + maxH + 2, idx - trainLen);
|
|
120
|
-
// «текстовая» часть карты: устойчивые описания бизнеса (профили готовятся отдельно, здесь только читаем)
|
|
121
240
|
const profiles = p.modelTextWeight > 0 ? loadProfiles() : null;
|
|
122
241
|
const f = buildFold(ds, { trainStart, testStart: idx, testEnd: idx + 1, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
|
|
123
242
|
const u = idx - f.lo;
|
|
124
|
-
const chosen = p.modelType in MODELS ? p.modelType : 'attention';
|
|
125
243
|
const list = [...new Set(explain ? Object.keys(MODELS) : ['attention', chosen])];
|
|
126
244
|
const fitted = {};
|
|
127
245
|
for (const n of list) {
|
|
128
246
|
fitted[n] = await MODELS[n].fit(f);
|
|
129
247
|
}
|
|
248
|
+
const snap = mapSnapshot(f);
|
|
249
|
+
// Освобождаем тяжёлые полные 90-дневные буферы признаков из памяти, оставляя только структурное ядро модели на день
|
|
250
|
+
const fTrain = {
|
|
251
|
+
N: f.N,
|
|
252
|
+
beta: f.beta,
|
|
253
|
+
sigma: f.sigma,
|
|
254
|
+
cos: f.cos,
|
|
255
|
+
emb: f.emb,
|
|
256
|
+
embN: f.embN,
|
|
257
|
+
eigenvalues: f.eigenvalues,
|
|
258
|
+
macro: f.macro ? { list: f.macro.list.map((m) => ({ id: m.id, label: m.label, b: m.b })) } : null,
|
|
259
|
+
};
|
|
260
|
+
const lightFitted = {
|
|
261
|
+
...fitted,
|
|
262
|
+
...(fitted.attention ? { attention: { ...fitted.attention, F: null } } : {}),
|
|
263
|
+
};
|
|
264
|
+
const trainedAt = Date.now();
|
|
265
|
+
trainedModelCache.set(fitKey, { dayMsk: barDayMsk, trainedAt, fTrain, fitted: lightFitted, snap });
|
|
266
|
+
|
|
130
267
|
if (liveSnap) applyLivePricesToFold(ds, f, u, fitted, liveSnap);
|
|
131
|
-
return
|
|
268
|
+
return {
|
|
269
|
+
...buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap),
|
|
270
|
+
snap,
|
|
271
|
+
trainedAt,
|
|
272
|
+
};
|
|
132
273
|
}
|
|
133
274
|
|
|
134
275
|
function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap = null) {
|
|
@@ -210,6 +351,14 @@ function attachDeltas(rows, stateObj) {
|
|
|
210
351
|
}
|
|
211
352
|
}
|
|
212
353
|
|
|
354
|
+
const fmtLiveBarLabel = (barTs, liveSnapTs) => {
|
|
355
|
+
if (liveSnapTs && (!barTs || liveSnapTs > barTs + BAR_MS)) {
|
|
356
|
+
const liveTime = fmtBarMsk(liveSnapTs).slice(-5);
|
|
357
|
+
return `${fmtBarMsk(barTs || liveSnapTs)} · котировки ${liveTime}`;
|
|
358
|
+
}
|
|
359
|
+
return fmtBarMsk(barTs || liveSnapTs || Date.now());
|
|
360
|
+
};
|
|
361
|
+
|
|
213
362
|
// Прогноз на ближайшие H часов с живым поминутным обновлением котировок (без переобучения тяжёлого окна каждую минуту)
|
|
214
363
|
export async function forecastNow(input, { force = false, onProgress } = {}) {
|
|
215
364
|
const p = normalizeParams(input);
|
|
@@ -223,18 +372,21 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
|
|
|
223
372
|
if (!force && baseState && baseState.key === key && baseState.lastTickResult && now - baseState.lastTickAt < 15000) {
|
|
224
373
|
return withNews(baseState.lastTickResult, p);
|
|
225
374
|
}
|
|
226
|
-
// 2) Если
|
|
227
|
-
if (!force && baseState && baseState.key === key && now - baseState.
|
|
375
|
+
// 2) Если срез свечей обновлялся менее 2 минут назад и нет force — делаем быстрый живой пересчёт (~80 мс запрос + 2 мс математика)
|
|
376
|
+
if (!force && baseState && baseState.key === key && now - baseState.dsLoadedAt < 2 * 60000) {
|
|
228
377
|
const liveSnap = await fetchLiveMoexSnapshot();
|
|
229
378
|
if (liveSnap) applyLivePricesToFold(baseState.ds, baseState.f, baseState.u, baseState.fitted, liveSnap);
|
|
230
379
|
const H = barsFromHours(effHorizon);
|
|
231
380
|
const r = buildPredictionRows(baseState.ds, p, baseState.last, baseState.f, baseState.u, baseState.fitted, baseState.chosen, H, effMinEdge, autoAdapt, true, liveSnap);
|
|
232
381
|
const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: baseState.snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
|
|
233
382
|
attachDeltas(sortedRows, baseState);
|
|
383
|
+
const liveTs = liveSnap?.ts || now;
|
|
384
|
+
const barTs = baseState.ds.timeline[baseState.last];
|
|
234
385
|
const result = {
|
|
235
386
|
...baseState.lastTickResult,
|
|
236
387
|
at: now,
|
|
237
|
-
liveTs
|
|
388
|
+
liveTs,
|
|
389
|
+
bar: fmtLiveBarLabel(barTs, liveSnap ? liveTs : null),
|
|
238
390
|
rows: sortedRows,
|
|
239
391
|
};
|
|
240
392
|
baseState.lastTickResult = result;
|
|
@@ -244,21 +396,26 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
|
|
|
244
396
|
|
|
245
397
|
if (!force && inflight.has(key)) return withNews(await inflight.get(key), p);
|
|
246
398
|
const task = (async () => {
|
|
399
|
+
const trainedToday = !force && isModelTrainedToday(p, now);
|
|
400
|
+
const windowDays = trainedToday ? 7 : Math.ceil(p.modelTrainDays * 1.5 + 25);
|
|
247
401
|
const [ds, liveSnap] = await Promise.all([
|
|
248
|
-
loadDataset({ fromTs: now -
|
|
402
|
+
loadDataset({ fromTs: now - windowDays * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, onProgress }),
|
|
249
403
|
fetchLiveMoexSnapshot(),
|
|
250
404
|
]);
|
|
251
405
|
const last = lastClosedIndex(ds, now);
|
|
252
|
-
const r = await predictAt(ds, p, last, { explain: true, liveSnap });
|
|
253
|
-
const snap = mapSnapshot(r.f);
|
|
406
|
+
const r = await predictAt(ds, p, last, { explain: true, liveSnap, forceTrain: Boolean(force) });
|
|
407
|
+
const snap = r.snap || mapSnapshot(r.f);
|
|
254
408
|
const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
|
|
255
409
|
const stateObj = baseState && baseState.key === key ? baseState : { prevExpMap: null, prevRankMap: null };
|
|
256
410
|
attachDeltas(sortedRows, stateObj);
|
|
411
|
+
const liveTs = liveSnap?.ts || now;
|
|
412
|
+
const barTs = ds.timeline[last];
|
|
257
413
|
const result = {
|
|
258
414
|
at: now,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
415
|
+
trainedAt: r.trainedAt || now,
|
|
416
|
+
liveTs,
|
|
417
|
+
barTs,
|
|
418
|
+
bar: fmtLiveBarLabel(barTs, liveSnap ? liveTs : null),
|
|
262
419
|
horizon: effHorizon, direction: p.modelDirection, model: r.chosen, thresholdPct: round(r.threshold * 100, 3), marketDown: r.marketDown,
|
|
263
420
|
rows: sortedRows,
|
|
264
421
|
map: { symbols: ds.tokens.map((t) => t.symbol), names: ds.tokens.map((t) => t.name), pts: snap.pts.map(([x, y]) => [round(x, 3), round(y, 3)]), cl: snap.clusters },
|
|
@@ -266,7 +423,8 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
|
|
|
266
423
|
};
|
|
267
424
|
baseState = {
|
|
268
425
|
key,
|
|
269
|
-
trainedAt: now,
|
|
426
|
+
trainedAt: r.trainedAt || now,
|
|
427
|
+
dsLoadedAt: now,
|
|
270
428
|
ds,
|
|
271
429
|
last,
|
|
272
430
|
f: r.f,
|
package/server/model/live.js
CHANGED
|
@@ -5,7 +5,7 @@ import { round, DAY, MSK, fmtBarMsk } from '../util.js';
|
|
|
5
5
|
import { hub } from '../hub.js';
|
|
6
6
|
import { latestBarTs } from '../data/moex.js';
|
|
7
7
|
import { loadDataset, barsFromHours, BAR_MS, BAR_INTERVAL } from './dataset.js';
|
|
8
|
-
import { predictAt, lastClosedIndex } from './forecast.js';
|
|
8
|
+
import { predictAt, lastClosedIndex, isModelTrainedToday } from './forecast.js';
|
|
9
9
|
import { computeReport, downsample } from './metrics.js';
|
|
10
10
|
import { MODELS } from './models.js';
|
|
11
11
|
import { blockingNews } from '../news/index.js';
|
|
@@ -90,12 +90,32 @@ class LiveEngine {
|
|
|
90
90
|
const status = returnPct < expPct - bandPct ? 'below' : returnPct > expPct + bandPct ? 'above' : 'inside';
|
|
91
91
|
corridor = { runId: s.benchmark.runId, expectedPct: expPct, bandPct, minPct: round(expPct - bandPct, 1), maxPct: round(expPct + bandPct, 1), histReturnPct: s.benchmark.returnPct, histMaxDdPct: s.benchmark.maxDrawdownPct, status };
|
|
92
92
|
}
|
|
93
|
+
const tradesList = [...(s.trades || [])].reverse().slice(0, 120).map((t) => ({
|
|
94
|
+
symbol: t.symbol,
|
|
95
|
+
side: t.side || 'long',
|
|
96
|
+
qty: t.qty || 0,
|
|
97
|
+
entryTs: t.entryTs,
|
|
98
|
+
exitTs: t.exitTs,
|
|
99
|
+
entryPrice: t.entryPrice || 0,
|
|
100
|
+
exitPrice: t.exitPrice || 0,
|
|
101
|
+
pnl: round(Number(t.pnl || 0), 2),
|
|
102
|
+
pnlPct: round(Number(t.pnlPct || 0), 2),
|
|
103
|
+
commission: round(Number(t.commission || 0), 2),
|
|
104
|
+
barsHeld: t.barsHeld || 0,
|
|
105
|
+
reasonEntry: t.reasonEntry || '',
|
|
106
|
+
reasonExit: t.reasonExit || '',
|
|
107
|
+
}));
|
|
108
|
+
const realizedPnl = round((s.trades || []).reduce((sum, t) => sum + (Number(t.pnl) || 0), 0), 2);
|
|
109
|
+
const totalCommission = round((s.trades || []).reduce((sum, t) => sum + (Number(t.commission) || 0), 0), 2);
|
|
93
110
|
return {
|
|
94
111
|
running: s.running, runId: s.runId, name: s.name || `Виртуальный счёт №${s.runId}`,
|
|
95
112
|
baseRunId: s.baseRunId || null, benchmark: s.benchmark || null, corridor,
|
|
96
113
|
params: s.params, startedAt: s.startedAt, stoppedAt: s.stoppedAt || null, lastBarTs: s.lastBarTs,
|
|
97
114
|
error: s.error, busy: s.busy, snapshot: snap, returnPct, pnl,
|
|
98
|
-
|
|
115
|
+
realizedPnl, totalCommission,
|
|
116
|
+
trades: tradesList,
|
|
117
|
+
curve: downsample(s.curve || [], 120),
|
|
118
|
+
recent: s.recent.slice(0, 25), metrics: s.metrics,
|
|
99
119
|
};
|
|
100
120
|
}
|
|
101
121
|
|
|
@@ -179,7 +199,9 @@ class LiveEngine {
|
|
|
179
199
|
if (!run || run.mode !== 'paper' || !st.params?.modelHorizon) continue;
|
|
180
200
|
const isRunning = Boolean(st.running && run.status === 'running');
|
|
181
201
|
const trades = getTrades(st.runId).map((t) => ({
|
|
182
|
-
symbol: t.symbol, side: t.side,
|
|
202
|
+
symbol: t.symbol, side: t.side, qty: t.qty, entryPrice: t.entry_price, exitPrice: t.exit_price,
|
|
203
|
+
pnl: t.pnl, pnlPct: t.pnl_pct, entryTs: t.entry_ts, exitTs: t.exit_ts, commission: t.commission,
|
|
204
|
+
reasonEntry: t.reason_entry, reasonExit: t.reason_exit,
|
|
183
205
|
barsHeld: Math.round((t.exit_ts - t.entry_ts) / 600000) * BAR_INTERVAL,
|
|
184
206
|
}));
|
|
185
207
|
const curve = st.curve || [];
|
|
@@ -239,7 +261,9 @@ class LiveEngine {
|
|
|
239
261
|
try {
|
|
240
262
|
const p = s.params;
|
|
241
263
|
const now = Date.now();
|
|
242
|
-
const
|
|
264
|
+
const trainedToday = isModelTrainedToday(p, now);
|
|
265
|
+
const windowDays = trainedToday ? 7 : Math.ceil(p.modelTrainDays * 1.5 + 25);
|
|
266
|
+
const ds = await loadDataset({ fromTs: now - windowDays * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends !== false });
|
|
243
267
|
s.lastSeenTs = latest;
|
|
244
268
|
changed = true;
|
|
245
269
|
const bySym = new Map(ds.tokens.map((t, i) => [t.symbol, i]));
|
|
@@ -308,7 +332,7 @@ class LiveEngine {
|
|
|
308
332
|
const dir = this.#direction(s);
|
|
309
333
|
const slots = dir === 'long' ? K : 2 * K;
|
|
310
334
|
const autoAdapt = s.params.modelAutoAdapt !== false;
|
|
311
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.
|
|
335
|
+
const maxSlotWeight = dir === 'long' ? Math.min(0.48, Math.max(1 / K, 1.4 / K)) : Math.min(0.48, 1.9 / (2 * K));
|
|
312
336
|
const invVol = s.params.modelAllocMode === 'invvol';
|
|
313
337
|
const effMinEdge = autoAdapt ? 2.5 : s.params.modelMinEdge;
|
|
314
338
|
const needPct = effMinEdge * 2 * (s.params.commissionPct + s.params.slippagePct);
|
|
@@ -320,7 +344,7 @@ class LiveEngine {
|
|
|
320
344
|
const side = o.action === 'short' ? 'short' : 'long';
|
|
321
345
|
const px = price(o.symbol);
|
|
322
346
|
if (s.held.has(o.symbol) || !(px > 0) || [...s.held.values()].filter((h) => h.side === side).length >= K) continue;
|
|
323
|
-
const conv = autoAdapt && needPct > 0 && o.expectedPct ? Math.min(1.
|
|
347
|
+
const conv = autoAdapt && needPct > 0 && o.expectedPct ? Math.min(1.25, Math.max(0.85, 0.80 + 0.25 * (Math.abs(o.expectedPct) / needPct))) : 1;
|
|
324
348
|
const baseAlloc = autoAdapt ? Math.min(s.equity * maxSlotWeight * conv, avail / Math.max(1, buyOrders.length - idx)) : Math.min(s.equity / slots, avail / Math.max(1, buyOrders.length - idx));
|
|
325
349
|
let alloc = baseAlloc;
|
|
326
350
|
if (invVol && o.weight > 0) {
|
|
@@ -356,7 +380,7 @@ class LiveEngine {
|
|
|
356
380
|
const autoAdapt = p.modelAutoAdapt !== false;
|
|
357
381
|
const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
|
|
358
382
|
const H = barsFromHours(effHorizon);
|
|
359
|
-
const stepBars = autoAdapt ? Math.max(
|
|
383
|
+
const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
|
|
360
384
|
if (s.barsSinceRebalance >= stepBars) {
|
|
361
385
|
await new Promise((r) => setImmediate(r));
|
|
362
386
|
const r = await predictAt(ds, p, idx);
|
package/server/model/research.js
CHANGED
|
@@ -312,8 +312,8 @@ async function run(runId, p, state) {
|
|
|
312
312
|
const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
|
|
313
313
|
const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
|
|
314
314
|
let curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
315
|
-
const stepBars = autoAdapt ? Math.max(
|
|
316
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.
|
|
315
|
+
const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
|
|
316
|
+
const maxSlotWeight = dir === 'long' ? Math.min(0.48, Math.max(1 / K, 1.4 / K)) : Math.min(0.48, 1.9 / (2 * K));
|
|
317
317
|
|
|
318
318
|
const closePos = (i, h, t, end, why) => {
|
|
319
319
|
const exitPrice = px(i, t);
|
|
@@ -405,7 +405,7 @@ async function run(runId, p, state) {
|
|
|
405
405
|
for (let idx = 0; idx < newEntries.length; idx++) {
|
|
406
406
|
const [i, side, edgeAt] = newEntries[idx];
|
|
407
407
|
const effEdge = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
|
|
408
|
-
const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.
|
|
408
|
+
const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.25, Math.max(0.85, 0.80 + 0.25 * (effEdge / needEdge))) : 1;
|
|
409
409
|
const baseAlloc = autoAdapt ? Math.min(equity * maxSlotWeight * conv, avail / Math.max(1, newEntries.length - idx)) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
|
|
410
410
|
let alloc = baseAlloc;
|
|
411
411
|
if (invVol && avgInv > 0 && sg[i] > 0) {
|