neuroalpha 0.5.17 → 0.5.20
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 +61 -60
- package/public/admin.html +604 -571
- package/public/app.js +101 -22
- package/public/landing.html +1207 -1273
- package/public/legal.html +6 -5
- package/public/splat3d.html +671 -0
- package/public/styles.css +12 -3
- package/scripts/set-admin-password.mjs +30 -0
- package/server/api/routes.js +55 -16
- package/server/auth/index.js +58 -13
- package/server/billing/yookassa.js +142 -133
- package/server/brokers/index.js +84 -52
- package/server/data/moex.js +6 -5
- package/server/db.js +1 -1
- package/server/index.js +3 -3
- package/server/instance/central.js +514 -505
- package/server/model/dataset.js +8 -9
- package/server/model/edge.js +74 -19
- package/server/model/forecast.js +117 -20
- package/server/model/live.js +16 -18
- package/server/model/research.js +48 -17
- package/server/security.js +4 -1
- package/server/strategist/index.js +758 -757
- package/server/strategist/tools.js +707 -675
package/public/app.js
CHANGED
|
@@ -58,7 +58,7 @@ const KNOWN_MOEX_TICKERS = new Set([
|
|
|
58
58
|
'AQUA', 'FESH', 'ASTR', 'SOFL', 'DATA', 'WUSH', 'LENT', 'BELU', 'ETLN', 'LSRG',
|
|
59
59
|
'SELG', 'UGLD', 'RAGR', 'AGRO', 'FIVE', 'NMTP', 'UPRO', 'MSNG', 'ELFV', 'TGKA',
|
|
60
60
|
'BANEP', 'BANE', 'RNFT', 'SGZH', 'MTLR', 'MTLRP', 'RASP', 'VSMO', 'ENPG', 'POLY',
|
|
61
|
-
'GEMC', 'MDMG', 'PRMD', 'APTK', 'CNRU', 'DELI', 'DIAS', 'EUTR', 'FIXP', 'HNFG',
|
|
61
|
+
'GEMC', 'MDMG', 'PRMD', 'APTK', 'CNRU', 'DELI', 'DIAS', 'EUTR', 'FIXP', 'FIXR', 'BAZA', 'UWGN', 'HNFG',
|
|
62
62
|
'IVAT', 'LEAS', 'MBNK', 'MVID', 'OGKB', 'OKEY', 'RENI', 'RTKM', 'RTKMP', 'SPBE', 'SVAV',
|
|
63
63
|
]);
|
|
64
64
|
// Текст ИИ и новостей недоверенный: HTML после markdown всегда проходит через DOMPurify.
|
|
@@ -72,8 +72,14 @@ const md = (s) => {
|
|
|
72
72
|
? `${p1}<span class="sym-link" data-sym="${sym}" title="Открыть досье акции ${sym}"><b>${sym}</b></span>`
|
|
73
73
|
: m
|
|
74
74
|
));
|
|
75
|
+
// Подсвечиваем розовым цветом строки таблиц, где указана блокировка входа (защита от сквиза, запрет шорта, падающий нож)
|
|
76
|
+
const withBlockedRows = withSyms.replace(/<tr>([\s\S]*?)<\/tr>/g, (m, inner) => (
|
|
77
|
+
!inner.includes('<th') && /(🔴|⛔|🚫|заблокирован|запрещ[её]н|защита от сквиза|без шорта|падающ[а-я]*\s+нож)/i.test(inner)
|
|
78
|
+
? `<tr class="fc-blocked">${inner}</tr>`
|
|
79
|
+
: m
|
|
80
|
+
));
|
|
75
81
|
// широкие таблицы листаются внутри своей обёртки, а не растягивают ленту чата
|
|
76
|
-
const withWraps =
|
|
82
|
+
const withWraps = withBlockedRows.replace(/<table>/g, '<div class="tbl-wrap"><table>').replace(/<\/table>/g, '</table></div>');
|
|
77
83
|
return DOMPurify.sanitize(withWraps, { FORBID_TAGS: ['style', 'form', 'input', 'button', 'textarea', 'select'], FORBID_ATTR: ['style'] });
|
|
78
84
|
};
|
|
79
85
|
const chartTime = (ts) => Math.floor((ts + MSK) / 1000);
|
|
@@ -985,6 +991,7 @@ async function loadForecast(force, silent = false) {
|
|
|
985
991
|
const f = await api(`/api/model/forecast${force ? '?force=1' : ''}`, { timeoutMs: force ? 90000 : 18000 });
|
|
986
992
|
// Считаем клиентские дельты и смещения в рейтинге относительно предыдущего кадра
|
|
987
993
|
f.rows.forEach((r, idx) => {
|
|
994
|
+
KNOWN_MOEX_TICKERS.add(r.symbol);
|
|
988
995
|
const rank = idx + 1;
|
|
989
996
|
if (prevClientExp.has(r.symbol)) {
|
|
990
997
|
const d = Number((r.expectedPct - prevClientExp.get(r.symbol)).toFixed(3));
|
|
@@ -1015,23 +1022,91 @@ async function loadForecast(force, silent = false) {
|
|
|
1015
1022
|
if (rd < 0) return ` <span class="neg small" title="Опустилась на ${Math.abs(rd)} поз. в рейтинге">↓${Math.abs(rd)}</span>`;
|
|
1016
1023
|
return '';
|
|
1017
1024
|
};
|
|
1018
|
-
const
|
|
1019
|
-
const
|
|
1025
|
+
const buyThr = f.buyThresholdPct ?? f.thresholdPct;
|
|
1026
|
+
const shortThr = f.shortThresholdPct ?? f.thresholdPct;
|
|
1027
|
+
const buyThrT2 = f.buyThresholdTier2Pct ?? (buyThr * 1.2);
|
|
1028
|
+
const shortThrT2 = f.shortThresholdTier2Pct ?? (shortThr * 1.2);
|
|
1029
|
+
const TIER1_SET = new Set([
|
|
1030
|
+
'SBER', 'SBERP', 'LKOH', 'GAZP', 'T', 'YDEX', 'ROSN', 'NVTK', 'GMKN', 'PLZL',
|
|
1031
|
+
'VTBR', 'TATN', 'TATNP', 'SNGS', 'SNGSP', 'ALRS', 'CHMF', 'NLMK', 'MAGN', 'MOEX',
|
|
1032
|
+
'OZON', 'HEAD', 'PHOR', 'AFLT', 'MTSS', 'IRAO', 'TRNFP', 'RUAL', 'FLOT', 'X5',
|
|
1033
|
+
]);
|
|
1034
|
+
const colgroup = `<colgroup><col style="width:27%"><col style="width:11%"><col style="width:11%"><col style="width:10%"><col style="width:13%"><col style="width:18%"><col style="width:10%"></colgroup>`;
|
|
1035
|
+
const blockInfoOf = (r, side) => {
|
|
1036
|
+
const reasons = [];
|
|
1037
|
+
let label = '';
|
|
1038
|
+
if (side === 'short' || (side !== 'long' && r.expectedPct < 0)) {
|
|
1039
|
+
const squeeze = Boolean(r.shortBlockedBySqueeze || (r.expectedPct < 0 && (r.lastMovePct ?? 0) > 4.5));
|
|
1040
|
+
const noShort = Boolean(r.shortBlockedByBroker || r.shortAllowed === false);
|
|
1041
|
+
if (squeeze) {
|
|
1042
|
+
reasons.push(`Аварийная защита от вертикального сквиза / планки: бумага взлетела на текущей свече (+${fmtNum(r.lastMovePct, 2)}% > +4,5%), открытие шорта запрещено`);
|
|
1043
|
+
label = '⛔ сквиз';
|
|
1044
|
+
}
|
|
1045
|
+
if (noShort) {
|
|
1046
|
+
reasons.push('Блокировка шорта: брокер запрещает короткие продажи по этой бумаге (shortEnabledFlag = false)');
|
|
1047
|
+
label = label ? '⛔ сквиз · шорт' : '⛔ без шорта';
|
|
1048
|
+
}
|
|
1049
|
+
if (r.shortBlockedByDiv) {
|
|
1050
|
+
reasons.push(`Защита от дивидендной отсечки: ${r.shortBlockedByDiv}`);
|
|
1051
|
+
if (!label) label = '⛔ отсечка';
|
|
1052
|
+
}
|
|
1053
|
+
if (r.shortBlockedByNews) {
|
|
1054
|
+
reasons.push(`Фильтр новостей (сильный позитив): ${r.shortBlockedByNews}`);
|
|
1055
|
+
if (!label) label = '⛔ новости';
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
const dip = Boolean(r.buyBlockedByDip || (r.expectedPct > 0 && (r.lastMovePct ?? 0) < -4.5));
|
|
1059
|
+
if (dip) {
|
|
1060
|
+
reasons.push(`Аварийная защита от обвала свечи: бумага падает на текущей свече (${fmtNum(r.lastMovePct, 2)}% < −4,5%), покупка запрещена`);
|
|
1061
|
+
label = '⛔ провал';
|
|
1062
|
+
}
|
|
1063
|
+
if (r.buyBlockedByDiv) {
|
|
1064
|
+
reasons.push(`Защита от дивидендного гэпа: ${r.buyBlockedByDiv}`);
|
|
1065
|
+
if (!label) label = '⛔ отсечка';
|
|
1066
|
+
}
|
|
1067
|
+
if (r.buyBlockedByNews) {
|
|
1068
|
+
reasons.push(`Фильтр новостей (сильный негатив): ${r.buyBlockedByNews}`);
|
|
1069
|
+
if (!label) label = '⛔ новости';
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (r.liquid === false) {
|
|
1073
|
+
reasons.push('Ставки запрещены из-за низкой ликвидности бумаги');
|
|
1074
|
+
if (!label) label = '⛔ неликвид';
|
|
1075
|
+
}
|
|
1076
|
+
return reasons.length ? { blocked: true, label, title: reasons.join(' · ') } : { blocked: false, label: '', title: '' };
|
|
1077
|
+
};
|
|
1078
|
+
const sigBadge = (r, blk) => {
|
|
1020
1079
|
if (r.signal) return '<span class="tag ok">купить</span>';
|
|
1021
1080
|
if (r.shortSignal) return '<span class="tag no">шорт</span>';
|
|
1022
|
-
if (
|
|
1081
|
+
if (blk?.blocked) return `<span class="tag blocked" title="${esc(blk.title)}">${esc(blk.label)}</span>`;
|
|
1023
1082
|
return '<span class="muted small">—</span>';
|
|
1024
1083
|
};
|
|
1025
|
-
const row = (r) =>
|
|
1084
|
+
const row = (r, side) => {
|
|
1085
|
+
const blk = !r.signal && !r.shortSignal ? blockInfoOf(r, side) : { blocked: false };
|
|
1086
|
+
const trCls = (r.signal || r.shortSignal) ? ' class="best"' : blk.blocked ? ` class="fc-blocked" title="${esc(blk.title)}"` : '';
|
|
1087
|
+
const isShort = side === 'short' || (side !== 'long' && r.expectedPct < 0);
|
|
1088
|
+
const isT1 = r.tier ? r.tier === 1 : TIER1_SET.has(r.symbol);
|
|
1089
|
+
const thrVal = isShort
|
|
1090
|
+
? (r.shortThresholdPct ?? (isT1 ? shortThr : shortThrT2))
|
|
1091
|
+
: (r.buyThresholdPct ?? (isT1 ? buyThr : buyThrT2));
|
|
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}`;
|
|
1097
|
+
const thrTitle = isShort
|
|
1098
|
+
? `Индивидуальный порог шорта для ${r.symbol}: ниже −${fmtNum(thrVal, 2)}% (${tierDesc})`
|
|
1099
|
+
: `Индивидуальный порог покупки для ${r.symbol}: выше ${fmtPct(thrVal, 2)} (${tierDesc})`;
|
|
1100
|
+
return `<tr${trCls}>
|
|
1026
1101
|
<td><span class="sym-link" data-sym="${esc(r.symbol)}" title="Открыть досье акции ${esc(r.symbol)} (${esc(r.name)})"><b>${esc(r.symbol)}</b> <span class="muted small">${esc(r.name)}</span></span>${rankBadge(r)}${dot(r.news)}</td>
|
|
1027
1102
|
<td class="num" style="white-space:nowrap">${r.price ? `${fmtNum(r.price, r.price < 1 ? 5 : r.price < 10 ? 3 : 2)} ₽` : '—'}</td>
|
|
1028
1103
|
<td class="num ${cls(r.expectedPct)}" style="white-space:nowrap"><b>${fmtPct(r.expectedPct, 2)}</b></td>
|
|
1029
1104
|
<td class="num" style="white-space:nowrap">${arrowOf(r)}</td>
|
|
1030
|
-
<td style="text-align:center;white-space:nowrap">${sigBadge(r)}</td>
|
|
1105
|
+
<td style="text-align:center;white-space:nowrap">${sigBadge(r, blk)}</td>
|
|
1031
1106
|
<td class="muted small" style="white-space:nowrap">${(r.influences || []).slice(0, 3).map((x) => `<span class="sym-link" data-sym="${esc(x.symbol)}" title="Сосед по карте рынка: вклад ${x.contribPct > 0 ? '+' : ''}${x.contribPct}%, близость ${x.closeness}">${esc(x.symbol)}</span>`).join(', ')}</td>
|
|
1107
|
+
<td class="num muted small" style="white-space:nowrap" title="${esc(thrTitle)}">${thrText}</td>
|
|
1032
1108
|
</tr>`;
|
|
1033
|
-
|
|
1034
|
-
const shortThr = f.shortThresholdPct ?? f.thresholdPct;
|
|
1109
|
+
};
|
|
1035
1110
|
// бумаги, выбывшие из индекса, остаются в модели как контекст, но в таблицах не показываются
|
|
1036
1111
|
const shownRows = f.rows.filter((r) => r.inIndex !== false);
|
|
1037
1112
|
const shortRows = [
|
|
@@ -1039,10 +1114,10 @@ async function loadForecast(force, silent = false) {
|
|
|
1039
1114
|
...shownRows.filter((r) => !r.shortSignal).sort((a, b) => a.expectedPct - b.expectedPct),
|
|
1040
1115
|
].slice(0, 4);
|
|
1041
1116
|
// общая сетка колонок для обеих таблиц: столбцы выровнены по всей высоте блока
|
|
1042
|
-
const fcTable = (title, rows, extraStyle = '') => `<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></tr></thead><tbody>${rows.map(row).join('')}</tbody></table>`;
|
|
1043
|
-
box.innerHTML = `<div style="overflow-x:auto">${fcTable('Сильнее рынка', shownRows.slice(0, 6))}
|
|
1044
|
-
${fcTable('Слабее рынка', shortRows, ';margin-top:14px')}</div>
|
|
1045
|
-
<p class="muted small">Ожидание — движение относительно рынка в целом (авто-пересчёт каждые 15 сек по живым ценам Мосбиржи).
|
|
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>`;
|
|
1118
|
+
box.innerHTML = `<div style="overflow-x:auto">${fcTable('Сильнее рынка', shownRows.slice(0, 6), '', 'long')}
|
|
1119
|
+
${fcTable('Слабее рынка', shortRows, ';margin-top:14px', 'short')}</div>
|
|
1120
|
+
<p class="muted small">Ожидание — движение относительно рынка в целом (авто-пересчёт каждые 15 сек по живым ценам Мосбиржи). Точный индивидуальный порог каждой акции рассчитывается в колонке <b>«Порог»</b> с учётом её ликвидности (оборота торгов), волатильности свечи и тренда индекса. <span style="color:var(--blocked-fg)">Розовым</span> отмечены строки, по которым вход запрещён (запрет шорта у брокера, отсечка дивидендов, фильтр новостей или экстремальная свеча ±4,5%).</p>`;
|
|
1046
1121
|
if (state.brokers?.accounts?.length) renderBrokers();
|
|
1047
1122
|
} catch (e) {
|
|
1048
1123
|
if (!silent) box.innerHTML = `<span class="neg">Не получилось: ${esc(e.message)}</span>`;
|
|
@@ -3629,10 +3704,10 @@ async function renderCloudPane() {
|
|
|
3629
3704
|
Брокерских счетов: до <b>${u.limits?.maxBrokerAccounts ?? 0}</b> (${capLimStr}) · Режим торговли: <b>${dirLimStr}</b>${u.planUntil ? ` · Оплачен до ${fmtDate(u.planUntil)}` : ''}.
|
|
3630
3705
|
</div>
|
|
3631
3706
|
</div>
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
</div
|
|
3707
|
+
<div class="row-gap" style="flex-wrap:wrap">
|
|
3708
|
+
${me.cloudMode ? `<button type="button" id="btn-cloud-verify">Проверить оплату</button>` : ''}
|
|
3709
|
+
${(me.cloudMode || me.role === 'instance' || !isDefaultLocal) ? `<button type="button" id="btn-cloud-logout">Выйти</button>` : ''}
|
|
3710
|
+
</div>
|
|
3636
3711
|
</div>
|
|
3637
3712
|
<div class="srow sblock">
|
|
3638
3713
|
<div class="stxt">
|
|
@@ -3984,9 +4059,13 @@ async function openStockModal(symbol) {
|
|
|
3984
4059
|
? '<span class="tag ok">сигнал: купить</span>'
|
|
3985
4060
|
: fc?.shortSignal
|
|
3986
4061
|
? '<span class="tag no">сигнал: шорт</span>'
|
|
3987
|
-
: fc?.
|
|
3988
|
-
? '<span class="tag"
|
|
3989
|
-
:
|
|
4062
|
+
: fc?.shortBlockedBySqueeze
|
|
4063
|
+
? '<span class="tag blocked" title="Аварийная защита от сквиза: бумага взлетела на текущей свече (> +4,5%)">⛔ защита от сквиза</span>'
|
|
4064
|
+
: fc?.shortBlockedByBroker
|
|
4065
|
+
? '<span class="tag blocked" title="Брокер запрещает короткие продажи по этой бумаге">⛔ шорт запрещён брокером</span>'
|
|
4066
|
+
: fc?.buyBlockedByDip
|
|
4067
|
+
? '<span class="tag blocked" title="Аварийная защита от обвала: бумага падает на текущей свече (< −4,5%)">⛔ защита от провала свечи</span>'
|
|
4068
|
+
: '<span class="tag">нейтрально (вне порога)</span>';
|
|
3990
4069
|
const shortBadge = d.shortAllowedByBroker === true
|
|
3991
4070
|
? '<span class="tag ok" title="Доступна для маржинальных коротких продаж в Т-Банке">шорт в Т-Банке: разрешён ✓</span>'
|
|
3992
4071
|
: d.shortAllowedByBroker === false
|
|
@@ -4003,7 +4082,7 @@ async function openStockModal(symbol) {
|
|
|
4003
4082
|
const kpiHtml = `<div class="kpis">
|
|
4004
4083
|
<div class="kpi"><div class="v">${d.price ? `${fmtNum(d.price, pxDec)} ₽` : '—'}</div><div class="l">текущая цена (1 лот = ${fmtNum(d.specs?.lotSize || 1, 0)} шт. ≈ ${fmtNum(d.specs?.lotCostRub || 0, 1)} ₽)</div></div>
|
|
4005
4084
|
<div class="kpi"><div class="v ${cls(d.dayChangePct)}">${fmtPct(d.dayChangePct, 2)}</div><div class="l">за день · за 30 дн.: <b class="${cls(d.monthChangePct)}">${fmtPct(d.monthChangePct, 1)}</b></div></div>
|
|
4006
|
-
<div class="kpi"><div class="v ${cls(fc?.expectedPct || 0)}">${fc ? fmtPct(fc.expectedPct, 2) : '—'}</div><div class="l">прогноз ИИ на ${fc?.horizonHours || 2.5} ч (порог +${fmtNum(d.buyThresholdPct, 2)}
|
|
4085
|
+
<div class="kpi"><div class="v ${cls(fc?.expectedPct || 0)}">${fc ? fmtPct(fc.expectedPct, 2) : '—'}</div><div class="l">прогноз ИИ на ${fc?.horizonHours || 2.5} ч (порог +${fmtNum(d.buyThresholdPct, 2)}%${d.shortThresholdPct ? ` / −${fmtNum(d.shortThresholdPct, 2)}%` : ''})</div></div>
|
|
4007
4086
|
<div class="kpi"><div class="v ${cls(fc?.lastMovePct || 0)}">${fc ? fmtPct(fc.lastMovePct, 2) : '—'}</div><div class="l">импульс текущей свечи (к IMOEX)</div></div>
|
|
4008
4087
|
<div class="kpi"><div class="v">${capStr}</div><div class="l">капитализация${d.specs?.indexWeightPct ? ` · вес ${d.specs.indexWeightPct}%` : ''}</div></div>
|
|
4009
4088
|
</div>`;
|
|
@@ -4023,7 +4102,7 @@ async function openStockModal(symbol) {
|
|
|
4023
4102
|
const infHtml = fc?.influences?.length
|
|
4024
4103
|
? `<div class="scard spadded">
|
|
4025
4104
|
<div style="font-weight:700;margin-bottom:6px">На что смотрит ИИ по ${esc(d.symbol)} (ближайшие соседи на спектральной карте рынка)</div>
|
|
4026
|
-
<div class="muted small" style="margin-bottom:8px">Вклад собственного движения акции (
|
|
4105
|
+
<div class="muted small" style="margin-bottom:8px">Вклад собственного движения акции (возврат к среднему после локального импульса): <b class="${cls(fc.selfPct)}">${fmtPct(fc.selfPct, 2)}</b> · Влияние связанных бумаг (переток импульса по карте):</div>
|
|
4027
4106
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px">
|
|
4028
4107
|
${fc.influences.map((inf) => `<div class="stab-box" style="padding:8px 10px;display:flex;justify-content:space-between;align-items:center">
|
|
4029
4108
|
<div>
|