neuroalpha 0.5.20 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +29 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuroalpha",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
package/public/app.js CHANGED
@@ -2442,6 +2442,11 @@ function toggleBrokerSpecs(id) {
2442
2442
  else hideBrokerSpecsById.add(id);
2443
2443
  saveNavPatch({ hideBrokerSpecs: [...hideBrokerSpecsById] });
2444
2444
  }
2445
+ let hideNonTradableAssets = loadSavedNav().hideNonTradableAssets !== false;
2446
+ function setHideNonTradableAssets(val) {
2447
+ hideNonTradableAssets = Boolean(val);
2448
+ saveNavPatch({ hideNonTradableAssets });
2449
+ }
2445
2450
  let editingBrokerId = null;
2446
2451
  let brokerPollBusy = false;
2447
2452
  let lastBrokerPollTs = 0;
@@ -2527,7 +2532,12 @@ function renderBrokers() {
2527
2532
  return p.qty;
2528
2533
  };
2529
2534
 
2535
+ const isNonTradablePos = (p) => !(Number(p.price) > 0 && Math.abs(Number(p.valueRub)) > 0) || Boolean(p.blocked);
2536
+
2530
2537
  const posStatus = (p) => {
2538
+ if (isNonTradablePos(p)) {
2539
+ return '<span class="tag" title="Бумага снята с торгов, проходит редомициляцию или заблокирована (рыночная оценка 0 ₽). Стратегия её не затрагивает">не торгуется (0 ₽)</span>';
2540
+ }
2531
2541
  if (p.instrumentType && p.instrumentType !== 'share') {
2532
2542
  const [kl] = INST_KIND_LABEL[p.instrumentType] || ['Актив', ''];
2533
2543
  return `<span class="tag" title="Драгметаллы, валюта, облигации и БПИФы не затрагиваются стратегией акций">${esc(kl.toLowerCase())} (защищён)</span>`;
@@ -2577,6 +2587,9 @@ function renderBrokers() {
2577
2587
  const totalPnlRub = ls?.totalPnlRub ?? (ls?.positions || []).reduce((s, p) => s + (p.pnlRub || 0), 0);
2578
2588
 
2579
2589
  // Вкладка 1: Активы портфеля
2590
+ const allPositions = ls?.positions || [];
2591
+ const nonTradableCount = allPositions.filter(isNonTradablePos).length;
2592
+ const visiblePositions = hideNonTradableAssets ? allPositions.filter((p) => !isNonTradablePos(p)) : allPositions;
2580
2593
  const fmtOwnership = (p) => {
2581
2594
  if (!(p.issueSize > 0) || !(p.qty > 0)) return '<span class="muted small">—</span>';
2582
2595
  const pct = (p.qty / p.issueSize) * 100;
@@ -2585,8 +2598,8 @@ function renderBrokers() {
2585
2598
  const tip = `Доля владения в компании ${p.name || p.symbol}: ${fmtNum(p.qty, 0)} из ${fmtNum(p.issueSize, 0)} акций всего выпуска на Мосбирже`;
2586
2599
  return `<span title="${esc(tip)}" style="white-space:nowrap"><b>${pctStr}%</b></span>`;
2587
2600
  };
2588
- const posHtml = ls?.positions?.length
2589
- ? `${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) => {
2590
2603
  const [kLbl, kCls] = INST_KIND_LABEL[p.instrumentType || 'share'] || ['Актив', ''];
2591
2604
  const fcRow = fcRowMap?.get(p.symbol);
2592
2605
  const fcBadge = fcRow ? ` · <span class="${cls(fcRow.expectedPct)}" title="Прогноз модели на ближайшие 2,5 ч">прогноз ${fcRow.expectedPct > 0 ? '+' : ''}${fmtNum(fcRow.expectedPct, 2)}%</span>` : '';
@@ -2601,7 +2614,7 @@ function renderBrokers() {
2601
2614
  <td>${posStatus(p)}${fcBadge}</td>
2602
2615
  </tr>`;
2603
2616
  }).join('')}</tbody></table>`
2604
- : `<p class="muted small">${ls?.ok ? 'Открытых позиций на счёте сейчас нет (все средства в рублях).' : 'Нажмите «Обновить портфель», чтобы загрузить баланс и активы с брокера.'}</p>`;
2617
+ : `<p class="muted small">${ls?.ok ? (nonTradableCount ? `Торгуемых позиций на счёте сейчас нет (скрыто неторгуемых активов с нулевой оценкой: ${nonTradableCount}).` : 'Открытых позиций на счёте сейчас нет (все средства в рублях).') : 'Нажмите «Обновить портфель», чтобы загрузить баланс и активы с брокера.'}</p>`;
2605
2618
 
2606
2619
  // Вкладка 2: Торговля и заявки стратегии
2607
2620
  const activeOrdHtml = ls?.activeOrders?.length
@@ -2877,16 +2890,20 @@ function renderBrokers() {
2877
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>` : ''}
2878
2891
  </div>
2879
2892
  ${ls?.ok ? `<div class="kpis">
2880
- <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>
2881
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>
2882
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>
2883
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>
2884
2897
  </div>` : ''}
2885
2898
  ${ls && !ls.ok ? `<p class="neg small">Ошибка связи с брокером: ${esc(ls.error)}</p>` : ''}
2886
- <div class="brk-subtabs">
2887
- <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>
2888
2901
  <button type="button" data-btab="trade" data-bid="${esc(a.id)}" class="${curSubTab === 'trade' ? 'active' : ''}">⚡ Торговля и заявки${pb?.orders?.length ? ` (${pb.orders.length})` : ''}</button>
2889
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>
2890
2907
  </div>
2891
2908
  <div class="brk-subpane" ${curSubTab === 'assets' ? '' : 'hidden'}>${posHtml}</div>
2892
2909
  <div class="brk-subpane" ${curSubTab === 'trade' ? '' : 'hidden'}>${activeOrdHtml}${batchHtml}</div>
@@ -3243,6 +3260,12 @@ brokerForm.addEventListener('submit', async (e) => {
3243
3260
  });
3244
3261
 
3245
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
+ }
3246
3269
  const fltBtn = e.target.closest('button[data-hfilter]');
3247
3270
  if (fltBtn) {
3248
3271
  toggleBrokerHistFilter(fltBtn.dataset.bid, fltBtn.dataset.hfilter);