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 +1 -1
- package/public/admin.html +2 -1
- package/public/app.js +39 -15
- package/public/landing.html +1207 -1499
- package/public/legal.html +6 -5
- package/server/api/routes.js +15 -3
- package/server/auth/index.js +23 -9
- package/server/brokers/index.js +4 -3
- package/server/model/edge.js +34 -1
- package/server/model/forecast.js +9 -6
- package/server/model/research.js +9 -4
- package/server/security.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
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
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
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 сек по живым ценам Мосбиржи).
|
|
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 =
|
|
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>${
|
|
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">всего на счёте (${
|
|
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' : ''}">📦 Активы портфеля (${
|
|
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
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
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">
|