neuroalpha 0.5.5 → 0.5.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuroalpha",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
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?.at) {
1048
+ if (!state.forecast) {
1049
+ lastForecastReqAt = Date.now();
1047
1050
  loadForecast(false, false);
1048
- } else if (Date.now() - state.forecast.at >= 12000) {
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
  }
@@ -1517,17 +1517,15 @@ export async function prepareBrokerOrders(id, { onlyEntries = false } = {}) {
1517
1517
  const nowTs = Date.now();
1518
1518
  const entryTsMap = acc.entryTsBySymbol || {};
1519
1519
  const exitCooldownMap = acc.exitCooldownBySymbol || {};
1520
- const mskDate = new Date(nowTs + MSK);
1521
- const isEveningSession = (mskDate.getUTCHours() * 60 + mskDate.getUTCMinutes()) >= 19 * 60;
1522
- const eveningBoost = (autoAdapt && isEveningSession) ? 1.25 : 1.0;
1523
- // Спред-фильтр реального стакана по эшелонам ликвидности:
1524
- // для 1-го эшелона требуем минимум базовый порог (≥ 0.50%), а для 2–3 эшелона (широкий спред стакана) — в 1.35× выше (≥ 0.675%)
1520
+ // Спред-фильтр реального стакана по эшелонам ликвидности: для 2–3 эшелона порог выше в TIER2_EDGE_MULT раз.
1521
+ // Отдельного вечернего ужесточения нет: проверка 23.10.2026 (первые 70% и последние 30% истории) показала,
1522
+ // что порог ×1,25 после 19:00 МСК снижает итог на обеих частях — правила совпадают с бэктестом.
1525
1523
  const baseBuyNeedPct = Number(fc.buyThresholdPct ?? fc.thresholdPct ?? 0.50);
1526
1524
  const baseShortNeedPct = Number(fc.shortThresholdPct ?? fc.thresholdPct ?? 0.50);
1527
- const minBuyEdgePct = round(baseBuyNeedPct * eveningBoost, 3);
1528
- const minShortEdgePct = round(baseShortNeedPct * eveningBoost, 3);
1529
- const reqBuyEdgeOf = (sym) => round(Math.max(minBuyEdgePct * symbolEdgeMult(sym), 0.30 * eveningBoost), 3);
1530
- const reqShortEdgeOf = (sym) => round(Math.max(minShortEdgePct * symbolEdgeMult(sym), 0.31 * eveningBoost), 3);
1525
+ const minBuyEdgePct = round(baseBuyNeedPct, 3);
1526
+ const minShortEdgePct = round(baseShortNeedPct, 3);
1527
+ const reqBuyEdgeOf = (sym) => round(Math.max(minBuyEdgePct * symbolEdgeMult(sym), 0.30), 3);
1528
+ const reqShortEdgeOf = (sym) => round(Math.max(minShortEdgePct * symbolEdgeMult(sym), 0.31), 3);
1531
1529
  const isInCooldown = (sym) => autoAdapt && exitCooldownMap[sym] && (nowTs - exitCooldownMap[sym] < 60 * 60000);
1532
1530
  // Самообучение на истории реальных сделок счёта (адаптивный карантин убыточных бумаг, как в research.js):
1533
1531
  // если за последние 14 дней по бумаге было >= 3 закрытых сделок на счёте и их суммарный итог отрицательный (< -1.0%),
@@ -2957,6 +2955,8 @@ async function tickBrokerAutoRefresh() {
2957
2955
  if (acc.broker === 'tbank' && (!acc.lastHistory?.at || Date.now() - acc.lastHistory.at >= 120000)) {
2958
2956
  await fetchBrokerHistory(acc.id, { year: curYear }).catch(() => {});
2959
2957
  }
2958
+ // Поддерживаем живой кэш прогноза всегда горячим в памяти сервера (даже когда браузер свёрнут)
2959
+ await forecastNow(getSettings()).catch(() => {});
2960
2960
  }
2961
2961
  });
2962
2962
  }
@@ -452,16 +452,32 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
452
452
 
453
453
  // 2б) Сразу после перезапуска сервера (пока baseState ещё не прогрет в RAM):
454
454
  // если в SQLite есть свежий снимок прогноза с теми же настройками (моложе 4 часов),
455
- // мгновенно отдаём его пользователю за 0 мс, а пересчёт свежих котировок запускаем параллельно в фоне!
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: true }).catch(() => {});
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
- return withNews(saved.result, p);
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: finishNow,
531
+ // если это был быстрый локальный прогрев после старта сервера, ставим dsLoadedAt так, чтобы через 15 секунд мягко догрузились свежие свечи с биржи
532
+ dsLoadedAt: coldLocalFirst ? (finishNow - 9 * 60000 - 45000) : finishNow,
513
533
  ds,
514
534
  last,
515
535
  f: r.f,