neuroalpha 0.5.4 → 0.5.6
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/package.json +1 -1
- package/public/app.js +10 -2
- package/server/brokers/index.js +6 -14
- package/server/model/forecast.js +25 -5
- package/server/model/models.js +4 -2
- package/server/model/research.js +3 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/public/app.js
CHANGED
|
@@ -990,6 +990,7 @@ async function loadForecast(force, silent = false) {
|
|
|
990
990
|
prevClientRank.set(r.symbol, rank);
|
|
991
991
|
});
|
|
992
992
|
state.forecast = f;
|
|
993
|
+
lastForecastReqAt = Date.now();
|
|
993
994
|
const liveTimeStr = new Date((f.liveTs || f.at || Date.now()) + MSK).toISOString().slice(11, 19);
|
|
994
995
|
$('#fc-title').textContent = `Прогноз на ${f.horizon} ч · свеча ${f.bar.slice(8, 10)}.${f.bar.slice(5, 7)} ${f.bar.slice(11)}`;
|
|
995
996
|
const badge = $('#fc-live-badge');
|
|
@@ -1041,14 +1042,18 @@ async function loadForecast(force, silent = false) {
|
|
|
1041
1042
|
}
|
|
1042
1043
|
}
|
|
1043
1044
|
const FORECAST_TICK_MS = 15000; // живое обновление каждые 15 секунд
|
|
1045
|
+
let lastForecastReqAt = 0;
|
|
1044
1046
|
function refreshForecastIfStale() {
|
|
1045
1047
|
if (forecastBusy && Date.now() - forecastBusyAt < 25000) return;
|
|
1046
|
-
if (!state.forecast
|
|
1048
|
+
if (!state.forecast) {
|
|
1049
|
+
lastForecastReqAt = Date.now();
|
|
1047
1050
|
loadForecast(false, false);
|
|
1048
|
-
} else if (Date.now() -
|
|
1051
|
+
} else if (Date.now() - lastForecastReqAt >= 12000) {
|
|
1052
|
+
lastForecastReqAt = Date.now();
|
|
1049
1053
|
loadForecast(false, true);
|
|
1050
1054
|
}
|
|
1051
1055
|
}
|
|
1056
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refreshForecastIfStale(); });
|
|
1052
1057
|
setInterval(() => { if (!document.hidden) refreshForecastIfStale(); }, FORECAST_TICK_MS);
|
|
1053
1058
|
|
|
1054
1059
|
// ---------- новости ----------
|
|
@@ -2966,6 +2971,9 @@ function connectWs() {
|
|
|
2966
2971
|
triggerTradingPulse(m.text || 'Расчёт прогноза…', 2500);
|
|
2967
2972
|
} else if (m.kind === 'radar') {
|
|
2968
2973
|
triggerTradingPulse(`Радар обновлён · сигналов: ${m.signals ?? 0}`, 1600);
|
|
2974
|
+
if (!state.forecast?.at || (m.ts && m.ts > state.forecast.at + 2000)) {
|
|
2975
|
+
refreshForecastIfStale();
|
|
2976
|
+
}
|
|
2969
2977
|
}
|
|
2970
2978
|
break;
|
|
2971
2979
|
}
|
package/server/brokers/index.js
CHANGED
|
@@ -354,7 +354,7 @@ function computeExecutionAudit(a) {
|
|
|
354
354
|
slipTier1Pct,
|
|
355
355
|
slipTier2Pct,
|
|
356
356
|
totalCostPerSidePct,
|
|
357
|
-
breakEvenTargetPct: 0.
|
|
357
|
+
breakEvenTargetPct: 0.237,
|
|
358
358
|
grossRub,
|
|
359
359
|
feesRub: round(feesRub, 0),
|
|
360
360
|
netRub,
|
|
@@ -1498,20 +1498,10 @@ export async function prepareBrokerOrders(id, { onlyEntries = false } = {}) {
|
|
|
1498
1498
|
const rawTopK = Number(params.modelTopK ?? 2);
|
|
1499
1499
|
const autoK = rawTopK === 0 || autoAdapt;
|
|
1500
1500
|
const K = rawTopK > 0 ? rawTopK : 4;
|
|
1501
|
-
|
|
1501
|
+
// Отбор до K кандидатов, прошедших абсолютный порог выгоды (единый с бэктестом в research.js)
|
|
1502
|
+
const filterBrokerDynamicTopK = (cands, _needVal, maxCount) => {
|
|
1502
1503
|
if (!cands.length || maxCount <= 0) return [];
|
|
1503
|
-
|
|
1504
|
-
if (!autoK || lim <= 1) return cands.slice(0, lim);
|
|
1505
|
-
const bestE = Math.abs(cands[0].expectedPct || 0);
|
|
1506
|
-
const out = [cands[0]];
|
|
1507
|
-
for (let m = 1; m < lim; m++) {
|
|
1508
|
-
const curE = Math.abs(cands[m].expectedPct || 0);
|
|
1509
|
-
const relMin = m === 1 ? 0.62 : 0.78;
|
|
1510
|
-
const absMin = m === 1 ? needVal * 1.15 : needVal * 1.45;
|
|
1511
|
-
if (curE >= bestE * relMin && curE >= absMin) out.push(cands[m]);
|
|
1512
|
-
else break;
|
|
1513
|
-
}
|
|
1514
|
-
return out;
|
|
1504
|
+
return cands.slice(0, Math.min(cands.length, maxCount));
|
|
1515
1505
|
};
|
|
1516
1506
|
// Автоматическая подстройка направления и мощности торговли под тариф и реальные данные счёта:
|
|
1517
1507
|
const marginAllowed = acc.lastSync?.marginEnabled !== false;
|
|
@@ -2967,6 +2957,8 @@ async function tickBrokerAutoRefresh() {
|
|
|
2967
2957
|
if (acc.broker === 'tbank' && (!acc.lastHistory?.at || Date.now() - acc.lastHistory.at >= 120000)) {
|
|
2968
2958
|
await fetchBrokerHistory(acc.id, { year: curYear }).catch(() => {});
|
|
2969
2959
|
}
|
|
2960
|
+
// Поддерживаем живой кэш прогноза всегда горячим в памяти сервера (даже когда браузер свёрнут)
|
|
2961
|
+
await forecastNow(getSettings()).catch(() => {});
|
|
2970
2962
|
}
|
|
2971
2963
|
});
|
|
2972
2964
|
}
|
package/server/model/forecast.js
CHANGED
|
@@ -452,16 +452,32 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
|
|
|
452
452
|
|
|
453
453
|
// 2б) Сразу после перезапуска сервера (пока baseState ещё не прогрет в RAM):
|
|
454
454
|
// если в SQLite есть свежий снимок прогноза с теми же настройками (моложе 4 часов),
|
|
455
|
-
// мгновенно
|
|
455
|
+
// мгновенно обновляем его цены и время радара по живым котировкам Т-Банка/Мосбиржи,
|
|
456
|
+
// а полный пересчёт модели из локальной базы SQLite запускаем параллельно в фоне!
|
|
456
457
|
if (!force && !baseState) {
|
|
457
458
|
const saved = kvGet('lastForecastSnap');
|
|
458
459
|
if (saved?.result && saved.key === key && now - (saved.result.at || 0) < 4 * 3600000) {
|
|
459
460
|
if (!inflight.has(key)) {
|
|
460
461
|
setImmediate(() => {
|
|
461
|
-
refreshDatasetStateInBackground(p, key, { force:
|
|
462
|
+
refreshDatasetStateInBackground(p, key, { force: false }).catch((err) => {
|
|
463
|
+
console.warn('[forecast] background warm-up error:', err?.message || err);
|
|
464
|
+
});
|
|
462
465
|
});
|
|
463
466
|
}
|
|
464
|
-
|
|
467
|
+
const liveSnap = await fetchLiveMoexSnapshot().catch(() => null);
|
|
468
|
+
const liveTs = liveSnap?.ts || now;
|
|
469
|
+
const rows = (saved.result.rows || []).map((r) => {
|
|
470
|
+
const px = liveSnap?.prices?.get(r.symbol);
|
|
471
|
+
return px > 0 ? { ...r, price: round(px, 4) } : r;
|
|
472
|
+
});
|
|
473
|
+
const updated = {
|
|
474
|
+
...saved.result,
|
|
475
|
+
at: now,
|
|
476
|
+
liveTs,
|
|
477
|
+
bar: fmtLiveBarLabel(saved.result.barTs || saved.result.at, liveSnap ? liveTs : null),
|
|
478
|
+
rows,
|
|
479
|
+
};
|
|
480
|
+
return withNews(updated, p);
|
|
465
481
|
}
|
|
466
482
|
}
|
|
467
483
|
|
|
@@ -479,8 +495,11 @@ function refreshDatasetStateInBackground(p, key, { force = false, onProgress } =
|
|
|
479
495
|
hub.emit({ type: 'trading:pulse', kind: 'train', ts: now, text: 'Обновление котировок и расчёт прогноза…' });
|
|
480
496
|
const trainedToday = !force && isModelTrainedToday(p, now);
|
|
481
497
|
const windowDays = trainedToday ? 7 : Math.ceil(p.modelTrainDays * 1.5 + 25);
|
|
498
|
+
// При первом холодном прогреве после перезапуска сервера (!baseState && !force) сначала читаем 100% локально из SQLite (за ~1-2 сек без похода в сеть по 100 акциям),
|
|
499
|
+
// а сетевую догрузку свежих свечей запускаем уже после того, как baseState готов в RAM!
|
|
500
|
+
const coldLocalFirst = !baseState && !force;
|
|
482
501
|
const [ds, liveSnap] = await Promise.all([
|
|
483
|
-
loadDataset({ fromTs: now - windowDays * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, onProgress }),
|
|
502
|
+
loadDataset({ fromTs: now - windowDays * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, localOnly: coldLocalFirst, onProgress }),
|
|
484
503
|
fetchLiveMoexSnapshot(),
|
|
485
504
|
]);
|
|
486
505
|
const last = lastClosedIndex(ds, now);
|
|
@@ -509,7 +528,8 @@ function refreshDatasetStateInBackground(p, key, { force = false, onProgress } =
|
|
|
509
528
|
baseState = {
|
|
510
529
|
key,
|
|
511
530
|
trainedAt: r.trainedAt || finishNow,
|
|
512
|
-
dsLoadedAt
|
|
531
|
+
// если это был быстрый локальный прогрев после старта сервера, ставим dsLoadedAt так, чтобы через 15 секунд мягко догрузились свежие свечи с биржи
|
|
532
|
+
dsLoadedAt: coldLocalFirst ? (finishNow - 9 * 60000 - 45000) : finishNow,
|
|
513
533
|
ds,
|
|
514
534
|
last,
|
|
515
535
|
f: r.f,
|
package/server/model/models.js
CHANGED
|
@@ -480,11 +480,13 @@ function predictAttention(m, f, u) {
|
|
|
480
480
|
s += ML[mi].b[i] * zz;
|
|
481
481
|
}
|
|
482
482
|
// Равный ансамбль 50/50 с базовой моделью своей инерции (при наличии весов inertiaCoef):
|
|
483
|
-
//
|
|
483
|
+
// если внимание к соседям и собственная инерция смотрят в разные стороны (s * sIn < 0),
|
|
484
|
+
// ослабляем сигнал вдвое, чтобы не проходить порог входа на конфликте моделей, но не искажать ранговый порядок IC!
|
|
484
485
|
if (m.inertiaCoef) {
|
|
485
486
|
let sIn = 0;
|
|
486
487
|
for (let q = 0; q < 3; q++) sIn += m.inertiaCoef[q] * f.X[0][q][k];
|
|
487
|
-
|
|
488
|
+
const mix = 0.50 * s + 0.50 * sIn;
|
|
489
|
+
s = s * sIn >= 0 ? mix : mix * 0.5;
|
|
488
490
|
}
|
|
489
491
|
out[i] = s;
|
|
490
492
|
}
|
package/server/model/research.js
CHANGED
|
@@ -279,24 +279,10 @@ async function run(runId, p, state) {
|
|
|
279
279
|
const rawTopK = Number(p.modelTopK ?? 2);
|
|
280
280
|
const autoK = rawTopK === 0 || autoAdapt;
|
|
281
281
|
const K = rawTopK > 0 ? rawTopK : 4;
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
// достаточно силён относительно лидера (>= 68% от силы №1) и уверенно перекрывает порог (>= 1.25 * need),
|
|
285
|
-
// чтобы не размывать капитал на слабые окраинные сигналы
|
|
286
|
-
const filterDynamicTopK = (cands, edgeFn, needVal, maxCount) => {
|
|
282
|
+
// Отбор до K кандидатов, прошедших абсолютный порог выгоды (без отсева 2..K из-за сравнения с лидером №1 — проверено на IS и OOS)
|
|
283
|
+
const filterDynamicTopK = (cands, _edgeFn, _needVal, maxCount) => {
|
|
287
284
|
if (!cands.length || maxCount <= 0) return [];
|
|
288
|
-
|
|
289
|
-
if (!autoK || lim <= 1) return cands.slice(0, lim);
|
|
290
|
-
const bestE = Math.abs(edgeFn(cands[0]));
|
|
291
|
-
const out = [cands[0]];
|
|
292
|
-
for (let m = 1; m < lim; m++) {
|
|
293
|
-
const curE = Math.abs(edgeFn(cands[m]));
|
|
294
|
-
const relMin = m === 1 ? 0.62 : 0.78;
|
|
295
|
-
const absMin = m === 1 ? needVal * 1.15 : needVal * 1.45;
|
|
296
|
-
if (curE >= bestE * relMin && curE >= absMin) out.push(cands[m]);
|
|
297
|
-
else break;
|
|
298
|
-
}
|
|
299
|
-
return out;
|
|
285
|
+
return cands.slice(0, Math.min(cands.length, maxCount));
|
|
300
286
|
};
|
|
301
287
|
const dir = p.modelDirection;
|
|
302
288
|
const exitRatio = Number(p.modelExitRatio ?? 0.5);
|