neuroalpha 0.5.19 → 0.5.21

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.19",
3
+ "version": "0.5.21",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
package/public/admin.html CHANGED
@@ -20,6 +20,7 @@
20
20
  --yellow: #eab308;
21
21
  }
22
22
  * { box-sizing: border-box; }
23
+ [hidden] { display: none !important; }
23
24
  body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
24
25
  a, a:visited { color: var(--accent); text-decoration: none; }
25
26
  a.brand, a.brand:visited { color: var(--text); }
@@ -197,7 +198,7 @@
197
198
  <section data-pane="oauth" hidden>
198
199
  <div class="card">
199
200
  <h2>Провайдеры быстрого входа (OAuth 2.0)</h2>
200
- <p class="sub">Настройка кнопок входа в 1 клик на витрине <code>/landing</code>. Скопируйте Callback URL в настройки соответствующего провайдера.</p>
201
+ <p class="sub">Настройка кнопок входа в 1 клик на витрине <code>/landing</code>. Иностранные провайдеры (Google) автоматически скрываются для пользователей из российского сегмента в соответствии с ФЗ № 406-ФЗ / 149-ФЗ. Скопируйте Callback URL в настройки соответствующего провайдера.</p>
201
202
  <form id="form-oauth" class="grid-form">
202
203
  <div id="oauth-rows" class="grid-form"></div>
203
204
  <div><button type="submit" class="primary">Сохранить OAuth-провайдеров</button></div>
package/public/app.js CHANGED
@@ -1089,10 +1089,11 @@ async function loadForecast(force, silent = false) {
1089
1089
  const thrVal = isShort
1090
1090
  ? (r.shortThresholdPct ?? (isT1 ? shortThr : shortThrT2))
1091
1091
  : (r.buyThresholdPct ?? (isT1 ? buyThr : buyThrT2));
1092
- const thrText = isShort ? `−${fmtNum(thrVal, 2)}%` : fmtPct(thrVal, 2);
1093
- const tierDesc = isT1
1094
- ? '1-й эшелон ликвидности (узкий спред стакана): базовый порог'
1095
- : '2–3-й эшелон (широкий спред стакана): порог повышен в 1,20× для покрытия реального проскальзывания';
1092
+ const thrFmt = Number(thrVal || 0).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
1093
+ const thrText = isShort ? `−${thrFmt}%` : `+${thrFmt}%`;
1094
+ const multStr = r.edgeMult ? `×${fmtNum(r.edgeMult, 2)}` : (isT1 ? '×1,00' : '×1,20');
1095
+ const sigStr = r.sigmaPct ? `, волатильность 10-мин свечи σ = ${fmtNum(r.sigmaPct, 2)}%` : '';
1096
+ const tierDesc = `${isT1 ? '1-й эшелон' : '2–3-й эшелон'}, индивидуальный множитель ${multStr}${sigStr}`;
1096
1097
  const thrTitle = isShort
1097
1098
  ? `Индивидуальный порог шорта для ${r.symbol}: ниже −${fmtNum(thrVal, 2)}% (${tierDesc})`
1098
1099
  : `Индивидуальный порог покупки для ${r.symbol}: выше ${fmtPct(thrVal, 2)} (${tierDesc})`;
@@ -1116,7 +1117,7 @@ async function loadForecast(force, silent = false) {
1116
1117
  const fcTable = (title, rows, extraStyle = '', side = 'long') => `<table class="fc-table" style="table-layout:fixed;width:100%${extraStyle}">${colgroup}<thead><tr><th>${title}</th><th class="num">Цена</th><th class="num">Ожидание</th><th class="num">Динамика</th><th style="text-align:center">Сигнал</th><th>Под влиянием</th><th class="num" title="Индивидуальный порог входа с учётом ликвидности бумаги (1-й или 2–3-й эшелон) и тренда индекса">Порог</th></tr></thead><tbody>${rows.map((r) => row(r, side)).join('')}</tbody></table>`;
1117
1118
  box.innerHTML = `<div style="overflow-x:auto">${fcTable('Сильнее рынка', shownRows.slice(0, 6), '', 'long')}
1118
1119
  ${fcTable('Слабее рынка', shortRows, ';margin-top:14px', 'short')}</div>
1119
- <p class="muted small">Ожидание — движение относительно рынка в целом (авто-пересчёт каждые 15 сек по живым ценам Мосбиржи). Индивидуальный порог каждой акции указан в колонке <b>«Порог»</b> (1-й эшелон / 2–3-й эшелон со спредом шире): «Купить» — ожидание выше <b>${fmtPct(buyThr, 2)} / ${fmtPct(buyThrT2, 2)}</b>.${f.direction && f.direction !== 'long' ? ` «Шорт» — ожидание ниже <b>−${fmtNum(shortThr, 2)}% / −${fmtNum(shortThrT2, 2)}%</b> (с учётом тренда индекса и доступности шорта у брокера).` : ''} <span style="color:var(--blocked-fg)">Розовым</span> отмечены строки, по которым вход запрещён (запрет шорта у брокера, отсечка дивидендов, фильтр новостей или экстремальная свеча ±4,5%).</p>`;
1120
+ <p class="muted small">Ожидание — движение относительно рынка в целом (авто-пересчёт каждые 15 сек по живым ценам Мосбиржи). Точный индивидуальный порог каждой акции рассчитывается в колонке <b>«Порог»</b> с учётом её ликвидности (оборота торгов), волатильности свечи и тренда индекса. <span style="color:var(--blocked-fg)">Розовым</span> отмечены строки, по которым вход запрещён (запрет шорта у брокера, отсечка дивидендов, фильтр новостей или экстремальная свеча ±4,5%).</p>`;
1120
1121
  if (state.brokers?.accounts?.length) renderBrokers();
1121
1122
  } catch (e) {
1122
1123
  if (!silent) box.innerHTML = `<span class="neg">Не получилось: ${esc(e.message)}</span>`;
@@ -2441,6 +2442,11 @@ function toggleBrokerSpecs(id) {
2441
2442
  else hideBrokerSpecsById.add(id);
2442
2443
  saveNavPatch({ hideBrokerSpecs: [...hideBrokerSpecsById] });
2443
2444
  }
2445
+ let hideNonTradableAssets = loadSavedNav().hideNonTradableAssets !== false;
2446
+ function setHideNonTradableAssets(val) {
2447
+ hideNonTradableAssets = Boolean(val);
2448
+ saveNavPatch({ hideNonTradableAssets });
2449
+ }
2444
2450
  let editingBrokerId = null;
2445
2451
  let brokerPollBusy = false;
2446
2452
  let lastBrokerPollTs = 0;
@@ -2526,7 +2532,12 @@ function renderBrokers() {
2526
2532
  return p.qty;
2527
2533
  };
2528
2534
 
2535
+ const isNonTradablePos = (p) => !(Number(p.price) > 0 && Math.abs(Number(p.valueRub)) > 0) || Boolean(p.blocked);
2536
+
2529
2537
  const posStatus = (p) => {
2538
+ if (isNonTradablePos(p)) {
2539
+ return '<span class="tag" title="Бумага снята с торгов, проходит редомициляцию или заблокирована (рыночная оценка 0 ₽). Стратегия её не затрагивает">не торгуется (0 ₽)</span>';
2540
+ }
2530
2541
  if (p.instrumentType && p.instrumentType !== 'share') {
2531
2542
  const [kl] = INST_KIND_LABEL[p.instrumentType] || ['Актив', ''];
2532
2543
  return `<span class="tag" title="Драгметаллы, валюта, облигации и БПИФы не затрагиваются стратегией акций">${esc(kl.toLowerCase())} (защищён)</span>`;
@@ -2576,6 +2587,9 @@ function renderBrokers() {
2576
2587
  const totalPnlRub = ls?.totalPnlRub ?? (ls?.positions || []).reduce((s, p) => s + (p.pnlRub || 0), 0);
2577
2588
 
2578
2589
  // Вкладка 1: Активы портфеля
2590
+ const allPositions = ls?.positions || [];
2591
+ const nonTradableCount = allPositions.filter(isNonTradablePos).length;
2592
+ const visiblePositions = hideNonTradableAssets ? allPositions.filter((p) => !isNonTradablePos(p)) : allPositions;
2579
2593
  const fmtOwnership = (p) => {
2580
2594
  if (!(p.issueSize > 0) || !(p.qty > 0)) return '<span class="muted small">—</span>';
2581
2595
  const pct = (p.qty / p.issueSize) * 100;
@@ -2584,8 +2598,8 @@ function renderBrokers() {
2584
2598
  const tip = `Доля владения в компании ${p.name || p.symbol}: ${fmtNum(p.qty, 0)} из ${fmtNum(p.issueSize, 0)} акций всего выпуска на Мосбирже`;
2585
2599
  return `<span title="${esc(tip)}" style="white-space:nowrap"><b>${pctStr}%</b></span>`;
2586
2600
  };
2587
- const posHtml = ls?.positions?.length
2588
- ? `${allocBarHtml}<table><thead><tr><th>Актив</th><th>Тип</th><th class="num">Кол-во</th><th class="num" title="Доля ваших акций в общем выпуске ценных бумаг компании-эмитента на Мосбирже (ISSUESIZE)">Доля владения</th><th class="num">Средняя → Текущая</th><th class="num">Стоимость (доля)</th><th class="num">Финрезультат</th><th>Статус и прогноз</th></tr></thead><tbody>${ls.positions.map((p) => {
2601
+ const posHtml = visiblePositions.length
2602
+ ? `${allocBarHtml}<table><thead><tr><th>Актив</th><th>Тип</th><th class="num">Кол-во</th><th class="num" title="Доля ваших акций в общем выпуске ценных бумаг компании-эмитента на Мосбирже (ISSUESIZE)">Доля владения</th><th class="num">Средняя → Текущая</th><th class="num">Стоимость (доля)</th><th class="num">Финрезультат</th><th>Статус и прогноз</th></tr></thead><tbody>${visiblePositions.map((p) => {
2589
2603
  const [kLbl, kCls] = INST_KIND_LABEL[p.instrumentType || 'share'] || ['Актив', ''];
2590
2604
  const fcRow = fcRowMap?.get(p.symbol);
2591
2605
  const fcBadge = fcRow ? ` · <span class="${cls(fcRow.expectedPct)}" title="Прогноз модели на ближайшие 2,5 ч">прогноз ${fcRow.expectedPct > 0 ? '+' : ''}${fmtNum(fcRow.expectedPct, 2)}%</span>` : '';
@@ -2600,7 +2614,7 @@ function renderBrokers() {
2600
2614
  <td>${posStatus(p)}${fcBadge}</td>
2601
2615
  </tr>`;
2602
2616
  }).join('')}</tbody></table>`
2603
- : `<p class="muted small">${ls?.ok ? 'Открытых позиций на счёте сейчас нет (все средства в рублях).' : 'Нажмите «Обновить портфель», чтобы загрузить баланс и активы с брокера.'}</p>`;
2617
+ : `<p class="muted small">${ls?.ok ? (nonTradableCount ? `Торгуемых позиций на счёте сейчас нет (скрыто неторгуемых активов с нулевой оценкой: ${nonTradableCount}).` : 'Открытых позиций на счёте сейчас нет (все средства в рублях).') : 'Нажмите «Обновить портфель», чтобы загрузить баланс и активы с брокера.'}</p>`;
2604
2618
 
2605
2619
  // Вкладка 2: Торговля и заявки стратегии
2606
2620
  const activeOrdHtml = ls?.activeOrders?.length
@@ -2876,16 +2890,20 @@ function renderBrokers() {
2876
2890
  ${a.executionAudit ? `<span class="k">Реальные издержки (сверка):</span><span class="v" title="Сверка фактических сделок счёта (после отключения автоследования) с ориентиром стресс-теста">комиссия <b>${fmtNum(a.executionAudit.commPct, 3)}%</b>${a.executionAudit.slipAllPct !== null ? ` · проскальзывание к сигналу <b>${a.executionAudit.slipAllPct > 0 ? '+' : ''}${fmtNum(a.executionAudit.slipAllPct, 3)}%</b>${a.executionAudit.slipTier1Pct !== null ? ` (1-й эш. ${a.executionAudit.slipTier1Pct > 0 ? '+' : ''}${fmtNum(a.executionAudit.slipTier1Pct, 3)}%${a.executionAudit.slipTier2Pct !== null ? `, 2–3-й эш. ${a.executionAudit.slipTier2Pct > 0 ? '+' : ''}${fmtNum(a.executionAudit.slipTier2Pct, 3)}%` : ''})` : ''}` : ''} · итого <b class="${a.executionAudit.totalCostPerSidePct <= a.executionAudit.breakEvenTargetPct ? 'pos' : 'neg'}">${fmtNum(a.executionAudit.totalCostPerSidePct, 3)}%/сторона</b> (порог безубыточности стресс-теста ≤ ${fmtNum(a.executionAudit.breakEvenTargetPct, 3)}%, закрыто ${a.executionAudit.closedCount} поз.${a.executionAudit.profitFactorGross !== null ? `, PF до ком. ${fmtNum(a.executionAudit.profitFactorGross, 2)}` : ''})</span>` : ''}
2877
2891
  </div>
2878
2892
  ${ls?.ok ? `<div class="kpis">
2879
- <div class="kpi"><div class="v">${fmtNum(ls.totalRub, 0)} ₽</div><div class="l">всего на счёте (${ls.positions?.length || 0} поз.)</div>${ls.marginInfo ? `<div class="sub">марж. запас: ${fmtNum(ls.marginInfo.freeMarginRub, 0)} ₽</div>` : ''}</div>
2893
+ <div class="kpi"><div class="v">${fmtNum(ls.totalRub, 0)} ₽</div><div class="l">всего на счёте (${visiblePositions.length} поз.${hideNonTradableAssets && nonTradableCount ? `, скрыто: ${nonTradableCount}` : ''})</div>${ls.marginInfo ? `<div class="sub">марж. запас: ${fmtNum(ls.marginInfo.freeMarginRub, 0)} ₽</div>` : ''}</div>
2880
2894
  <div class="kpi"><div class="v ${ls.cashRub > 1000 ? 'pos' : 'muted'}">${fmtNum(ls.cashRub, 0)} ₽</div><div class="l">свободные рубли для покупок</div>${ls.marginInfo?.startingMarginRub > 0 ? `<div class="sub">занято маржи: ${fmtNum(ls.marginInfo.startingMarginRub, 0)} ₽</div>` : ''}</div>
2881
2895
  <div class="kpi"><div class="v ${cls(totalPnlRub)}">${totalPnlRub >= 0 ? '+' : ''}${fmtNum(totalPnlRub, 0)} ₽</div><div class="l">финрезультат открытых позиций</div>${a.executionAudit?.periods ? `<div class="sub">за 30 дн.: <b class="${cls(a.executionAudit.periods.month.netRub)}">${a.executionAudit.periods.month.netRub > 0 ? '+' : ''}${fmtNum(a.executionAudit.periods.month.netRub, 0)} ₽</b></div>` : ''}</div>
2882
2896
  <div class="kpi"><div class="v">${fmtNum(strategyRub, 0)} ₽ <span class="muted small">/ ${fmtNum(protectedRub, 0)} ₽</span></div><div class="l">в стратегии / защищено</div>${a.executionAudit?.periods ? `<div class="sub">сегодня: <b class="${cls(a.executionAudit.periods.day.netRub)}">${a.executionAudit.periods.day.netRub > 0 ? '+' : ''}${fmtNum(a.executionAudit.periods.day.netRub, 0)} ₽</b></div>` : ''}</div>
2883
2897
  </div>` : ''}
2884
2898
  ${ls && !ls.ok ? `<p class="neg small">Ошибка связи с брокером: ${esc(ls.error)}</p>` : ''}
2885
- <div class="brk-subtabs">
2886
- <button type="button" data-btab="assets" data-bid="${esc(a.id)}" class="${curSubTab === 'assets' ? 'active' : ''}">📦 Активы портфеля (${ls?.positions?.length || 0})</button>
2899
+ <div class="brk-subtabs" style="align-items:center">
2900
+ <button type="button" data-btab="assets" data-bid="${esc(a.id)}" class="${curSubTab === 'assets' ? 'active' : ''}">📦 Активы портфеля (${visiblePositions.length})</button>
2887
2901
  <button type="button" data-btab="trade" data-bid="${esc(a.id)}" class="${curSubTab === 'trade' ? 'active' : ''}">⚡ Торговля и заявки${pb?.orders?.length ? ` (${pb.orders.length})` : ''}</button>
2888
2902
  <button type="button" data-btab="history" data-bid="${esc(a.id)}" class="${curSubTab === 'history' ? 'active' : ''}">📜 История счёта${lh?.totalCount ? ` (${lh.totalCount})` : ''}</button>
2903
+ <label style="margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted);cursor:pointer;user-select:none;padding:2px 4px" title="Скрывать из портфеля делистингованные, заблокированные и неторгуемые ценные бумаги с нулевой рыночной оценкой (0 ₽)">
2904
+ <input type="checkbox" data-bhide-nontradable="1" ${hideNonTradableAssets ? 'checked' : ''} style="margin:0;cursor:pointer">
2905
+ <span>Скрывать неторгуемые активы${nonTradableCount ? ` (${nonTradableCount})` : ''}</span>
2906
+ </label>
2889
2907
  </div>
2890
2908
  <div class="brk-subpane" ${curSubTab === 'assets' ? '' : 'hidden'}>${posHtml}</div>
2891
2909
  <div class="brk-subpane" ${curSubTab === 'trade' ? '' : 'hidden'}>${activeOrdHtml}${batchHtml}</div>
@@ -3242,6 +3260,12 @@ brokerForm.addEventListener('submit', async (e) => {
3242
3260
  });
3243
3261
 
3244
3262
  $('#brokers-trade').addEventListener('click', async (e) => {
3263
+ const hideChk = e.target.closest('input[data-bhide-nontradable]');
3264
+ if (hideChk) {
3265
+ setHideNonTradableAssets(hideChk.checked);
3266
+ renderBrokers();
3267
+ return;
3268
+ }
3245
3269
  const fltBtn = e.target.closest('button[data-hfilter]');
3246
3270
  if (fltBtn) {
3247
3271
  toggleBrokerHistFilter(fltBtn.dataset.bid, fltBtn.dataset.hfilter);
@@ -3703,10 +3727,10 @@ async function renderCloudPane() {
3703
3727
  Брокерских счетов: до <b>${u.limits?.maxBrokerAccounts ?? 0}</b> (${capLimStr}) · Режим торговли: <b>${dirLimStr}</b>${u.planUntil ? ` · Оплачен до ${fmtDate(u.planUntil)}` : ''}.
3704
3728
  </div>
3705
3729
  </div>
3706
- ${me.cloudMode ? `<div class="row-gap" style="flex-wrap:wrap">
3707
- <button type="button" id="btn-cloud-verify">Проверить оплату</button>
3708
- <button type="button" id="btn-cloud-logout">Выйти</button>
3709
- </div>` : ''}
3730
+ <div class="row-gap" style="flex-wrap:wrap">
3731
+ ${me.cloudMode ? `<button type="button" id="btn-cloud-verify">Проверить оплату</button>` : ''}
3732
+ ${(me.cloudMode || me.role === 'instance' || !isDefaultLocal) ? `<button type="button" id="btn-cloud-logout">Выйти</button>` : ''}
3733
+ </div>
3710
3734
  </div>
3711
3735
  <div class="srow sblock">
3712
3736
  <div class="stxt">