neuroalpha 0.3.12 → 0.3.13
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/LICENSE +13 -17
- package/README.md +4 -2
- package/bin/cli.js +1 -1
- package/deploy/install.sh +1 -1
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -1
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +1 -1
- package/package.json +2 -2
- package/public/admin.html +43 -20
- package/public/app.js +524 -211
- package/public/index.html +43 -3
- package/public/landing.html +328 -115
- package/public/legal.html +37 -16
- package/public/styles.css +24 -3
- package/server/api/routes.js +61 -29
- package/server/auth/index.js +109 -20
- package/server/billing/yookassa.js +1 -1
- package/server/brokers/index.js +179 -34
- package/server/config.js +1 -1
- package/server/data/moex.js +1 -1
- package/server/db.js +17 -3
- package/server/hub.js +1 -1
- package/server/index.js +4 -4
- package/server/llm/client.js +1 -1
- package/server/llm/providers.js +1 -1
- package/server/model/dataset.js +1 -1
- package/server/model/forecast.js +188 -17
- package/server/model/linalg.js +1 -1
- package/server/model/live.js +6 -19
- package/server/model/metrics.js +1 -1
- package/server/model/models.js +1 -1
- package/server/model/profiles.js +1 -1
- package/server/model/research.js +12 -36
- package/server/model/vocab.js +1 -1
- package/server/news/index.js +1 -1
- package/server/notify/index.js +1 -1
- package/server/params-meta.js +4 -3
- package/server/settings.js +1 -1
- package/server/strategist/analytics.js +2 -2
- package/server/strategist/index.js +2 -2
- package/server/strategist/tools.js +1 -1
- package/server/util.js +1 -1
package/public/legal.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Copyright (c) 2026 Roman A. Perepelkin.
|
|
1
|
+
<!-- Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software. -->
|
|
2
2
|
<!doctype html>
|
|
3
3
|
<html lang="ru">
|
|
4
4
|
<head>
|
|
@@ -41,22 +41,37 @@
|
|
|
41
41
|
|
|
42
42
|
<h3>1.1. Предмет договора и правовой статус сервиса</h3>
|
|
43
43
|
<ul>
|
|
44
|
-
<li>Исполнитель предоставляет Пользователю дистанционный доступ к функциям
|
|
45
|
-
<li>Сервис «НейроАльфа» является исключительно программно-техническим средством (калькулятором статистических закономерностей и интерфейсом автоматизации отправки поручений по
|
|
46
|
-
<li><b>Исполнитель не осуществляет деятельность по инвестиционному консультированию</b> (в смысле ст. 6.1 Федерального закона № 39-ФЗ «О рынке ценных бумаг»), не составляет индивидуальных инвестиционных профилей и не даёт индивидуальных инвестиционных
|
|
44
|
+
<li>Исполнитель предоставляет Пользователю дистанционный доступ к функциям облачного программно-вычислительного комплекса «НейроАльфа» (модель PaaS/SaaS) в соответствии с выбранным Пользователем тарифным планом («Старт», «Инвестор», «Профи», «VIP»).</li>
|
|
45
|
+
<li>Сервис «НейроАльфа» является исключительно программно-техническим средством (калькулятором статистических закономерностей и интерфейсом автоматизации отправки поручений по параметрам и предохранителям, заданным самим Пользователем).</li>
|
|
46
|
+
<li><b>Исполнитель не осуществляет деятельность по инвестиционному консультированию</b> (в смысле ст. 6.1 Федерального закона № 39-ФЗ «О рынке ценных бумаг»), не составляет индивидуальных инвестиционных профилей и не даёт индивидуальных инвестиционных рекомендаций (ИИР).</li>
|
|
47
47
|
<li><b>Исполнитель не осуществляет доверительное управление</b> или брокерскую деятельность, не принимает денежные средства или ценные бумаги Пользователей на свои счета. Все торговые операции совершаются Пользователем самостоятельно на его личном счёте у лицензированного брокера.</li>
|
|
48
48
|
</ul>
|
|
49
49
|
|
|
50
|
-
<h3>1.2.
|
|
50
|
+
<h3>1.2. Тарифные планы и технические параметры мощности</h3>
|
|
51
51
|
<ul>
|
|
52
|
-
<li
|
|
52
|
+
<li><b>Тариф «Старт (Тест-драйв)» (0 ₽):</b> предоставляет доступ к историческим исследованиям (экспериментам) и 1 виртуальному демо-счёту без возможности подключения реальных брокерских счетов.</li>
|
|
53
|
+
<li><b>Тариф «Инвестор»:</b> позволяет подключить до 2 реальных брокерских счетов с ограничением выделенного капитала до 1 000 000 ₽ на каждом счёте, работающих исключительно в режиме покупок («Только в рост (без шорта)») с консервативным профилем загрузки слота и предельным целевым темпом доходности стратегии до 50% годовых.</li>
|
|
54
|
+
<li><b>Тариф «Профи»:</b> позволяет подключить до 5 реальных брокерских счетов с ограничением выделенного капитала до 10 000 000 ₽ на каждом счёте, включает режим работы на росте и падении акций («В рост и в падение (с шортом)») со стандартным шагом ребалансировки и предельным целевым темпом доходности стратегии до 100% годовых.</li>
|
|
55
|
+
<li><b>Тариф «VIP (Без ограничений)»:</b> предоставляется по индивидуальному согласованию с Администрацией платформы (через персональный VIP-токен), снимает ограничения на количество счетов, объём торгового капитала и потолок годовой доходности, активируя полный режим мощности ядра (внутричасовое полушаговое сопровождение <code>H/2</code> и динамическую концентрацию слота <code>Conviction Sizing</code>).</li>
|
|
56
|
+
</ul>
|
|
57
|
+
|
|
58
|
+
<h3>1.3. Запрет использования автоследования и ретрансляции сигналов</h3>
|
|
59
|
+
<ul>
|
|
60
|
+
<li>Пользователю <b>строго запрещается</b> подключать к платформе «НейроАльфа» брокерские счета (субсчета), участвующие в любых сервисах автоследования или копирования сделок (как в роли автора/ведущего стратегии, так и в роли подписчика), включая, но не ограничиваясь: «Сигналы» Т-Инвестиций, Finam Comon, Автоследование БКС и сторонние копи-трейдинг сервисы.</li>
|
|
61
|
+
<li>Пользователю запрещается передавать, публиковать, ретранслировать или перепродавать третьим лицам формируемые платформой расчёты, прогнозы и пакеты торговых поручений.</li>
|
|
62
|
+
<li>Платформа оснащена автоматическим техническим контролем признаков автоследования. При обнаружении на подключённом субсчёте операций автоследования или ретрансляции сделок подача поручений по счёту автоматически блокируется, а действие подписки может быть прекращено Исполнителем в одностороннем порядке без возврата оплаченных средств.</li>
|
|
63
|
+
</ul>
|
|
64
|
+
|
|
65
|
+
<h3>1.4. Порядок оплаты и предоставления доступа</h3>
|
|
66
|
+
<ul>
|
|
67
|
+
<li>Оплата тарифных планов производится в рублях РФ безналичным способом (банковские карты МИР, Visa, Mastercard, Система быстрых платежей) либо путём активации промокода / персонального VIP-токена.</li>
|
|
53
68
|
<li>Доступ к оплаченному тарифному плану активируется автоматически в момент получения подтверждения об успешной оплате (`succeeded`) от платёжной системы на срок оплаченного периода (30, 90 или 365 календарных дней).</li>
|
|
54
|
-
<li>Услуга предоставления доступа к SaaS-платформе считается оказанной надлежащим образом с момента открытия технической возможности использования
|
|
69
|
+
<li>Услуга предоставления доступа к PaaS/SaaS-платформе считается оказанной надлежащим образом с момента открытия технической возможности использования выбранного тарифа в учётной записи Пользователя.</li>
|
|
55
70
|
</ul>
|
|
56
71
|
|
|
57
|
-
<h3>1.
|
|
72
|
+
<h3>1.5. Ограничение ответственности</h3>
|
|
58
73
|
<ul>
|
|
59
|
-
<li>Программное обеспечение предоставляется по принципу «как есть» (as is). Исполнитель не гарантирует достижения какой-либо положительной доходности при использовании аналитических расчётов
|
|
74
|
+
<li>Программное обеспечение предоставляется по принципу «как есть» (as is). Исполнитель не гарантирует достижения какой-либо положительной доходности при использовании аналитических расчётов программы. Указанные в описании тарифов предельные уровни годовой доходности (50% / 100% годовых) являются верхними техническими порогами алгоритма управления риском, а не обещанием или гарантией дохода.</li>
|
|
60
75
|
<li>Пользователь самостоятельно задаёт параметры стратегии, лимиты суммы, пороги дневного стопа и несёт полную имущественную ответственность за результаты сделок, совершаемых через подключённые им API-токены брокеров.</li>
|
|
61
76
|
</ul>
|
|
62
77
|
</section>
|
|
@@ -88,24 +103,30 @@
|
|
|
88
103
|
</section>
|
|
89
104
|
|
|
90
105
|
<section class="doc" id="requisites">
|
|
91
|
-
<h2>4.
|
|
106
|
+
<h2>4. Правообладатель, Оператор платформы и контакты поддержки</h2>
|
|
92
107
|
<div id="legal-req-body">
|
|
93
|
-
<
|
|
108
|
+
<ul>
|
|
109
|
+
<li><b>Оператор платформы (Исполнитель):</b> ООО «БИС» («Бизнес Интер Софт», г. Рязань)</li>
|
|
110
|
+
<li><b>Автор и правообладатель ПО «НейроАльфа»:</b> Перепелкин Роман Александрович</li>
|
|
111
|
+
<li><b>Лицензионный статус:</b> Проприетарное программное обеспечение (Все права защищены). Копирование, декомпиляция, модификация и распространение без письменного согласия Правообладателя запрещены.</li>
|
|
112
|
+
</ul>
|
|
94
113
|
</div>
|
|
95
114
|
</section>
|
|
96
|
-
<div style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">© 2026
|
|
115
|
+
<div style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">© 2026 Автор: Перепелкин Р.А. · Оператор: ООО «БИС» («Бизнес Интер Софт», г. Рязань) · Все права защищены</div>
|
|
97
116
|
</div>
|
|
98
117
|
<script>
|
|
99
118
|
fetch('/api/public/legal').then((r) => r.json()).then((d) => {
|
|
100
119
|
const rows = [
|
|
101
|
-
|
|
120
|
+
`<li><b>Оператор платформы (Исполнитель):</b> ${d.ownerName || 'ООО «БИС» («Бизнес Интер Софт»)'}${d.status ? ` (${d.status})` : ''}</li>`,
|
|
121
|
+
`<li><b>Автор и правообладатель ПО:</b> ${d.authorName || 'Перепелкин Роман Александрович'}</li>`,
|
|
102
122
|
d.inn ? `<li><b>ИНН:</b> ${d.inn}</li>` : '',
|
|
103
|
-
d.ogrn ? `<li><b
|
|
123
|
+
d.ogrn ? `<li><b>ОГРН:</b> ${d.ogrn}</li>` : '',
|
|
104
124
|
d.email ? `<li><b>Email технической поддержки:</b> <a href="mailto:${d.email}">${d.email}</a></li>` : '',
|
|
105
125
|
d.phone ? `<li><b>Телефон:</b> ${d.phone}</li>` : '',
|
|
106
|
-
|
|
126
|
+
`<li><b>Местонахождение:</b> ${d.address || 'Российская Федерация, г. Рязань'}</li>`,
|
|
127
|
+
'<li><b>Правовой режим ПО:</b> Проприетарное программное обеспечение (Все права защищены)</li>',
|
|
107
128
|
].filter(Boolean).join('');
|
|
108
|
-
document.getElementById('legal-req-body').innerHTML =
|
|
129
|
+
document.getElementById('legal-req-body').innerHTML = `<ul>${rows}</ul>`;
|
|
109
130
|
}).catch(() => {});
|
|
110
131
|
</script>
|
|
111
132
|
</body>
|
package/public/styles.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright (c) 2026 Roman A. Perepelkin.
|
|
1
|
+
/* Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software. */
|
|
2
2
|
:root {
|
|
3
3
|
--bg: #0d1016;
|
|
4
4
|
--panel: #13171f;
|
|
@@ -57,6 +57,9 @@ button.icon { padding: 2px 8px; color: var(--text); background: transparent; }
|
|
|
57
57
|
button.icon-lg { width: 40px; height: 40px; padding: 0; font-size: 15px; flex: none; }
|
|
58
58
|
|
|
59
59
|
main { display: grid; grid-template-columns: var(--chat-w) 5px 1fr; height: calc(100vh - 46px); }
|
|
60
|
+
body.chat-closed main { grid-template-columns: 1fr; }
|
|
61
|
+
body.chat-closed .chat, body.chat-closed #splitter { display: none; }
|
|
62
|
+
#btn-chat-toggle.active { border-color: var(--accent); color: var(--accent); background: rgba(79, 140, 255, 0.1); }
|
|
60
63
|
#splitter { cursor: col-resize; background: var(--line); transition: background .15s; }
|
|
61
64
|
#splitter:hover, #splitter.drag { background: var(--accent); }
|
|
62
65
|
body.dragging { cursor: col-resize; user-select: none; }
|
|
@@ -202,7 +205,7 @@ body.light .tbadge.run { background: #dbeafe; border-color: #93c5fd; }
|
|
|
202
205
|
.ld-syms { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
|
|
203
206
|
.ld-sym { font-size: 11px; padding: 2px 7px; border-radius: 5px; background: var(--bg); border: 1px solid var(--line); color: var(--text); }
|
|
204
207
|
.ld-sym.ok { border-color: rgba(63, 185, 80, 0.5); color: var(--green); }
|
|
205
|
-
.verdict { margin:
|
|
208
|
+
.verdict { margin: 10px 0; font-size: 13.5px; line-height: 1.45; }
|
|
206
209
|
.kpis { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
207
210
|
@media (max-width: 1180px) { .kpis { grid-template-columns: repeat(3, 1fr); } }
|
|
208
211
|
.kpi { background: var(--bg); padding: 10px 14px; cursor: help; }
|
|
@@ -253,6 +256,11 @@ tr.best td { background: var(--best-bg); }
|
|
|
253
256
|
.stab-chip { background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; font-size: 12px; }
|
|
254
257
|
.live-card { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin-top: 10px; }
|
|
255
258
|
.live-card.stopped { opacity: .75; }
|
|
259
|
+
.live-card.collapsed { padding: 10px 14px; }
|
|
260
|
+
.acc-fold-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; cursor: pointer; user-select: none; }
|
|
261
|
+
.acc-fold-head:hover b { color: var(--accent); }
|
|
262
|
+
.acc-caret { display: inline-block; width: 16px; color: var(--muted); font-size: 12px; transition: transform .15s; }
|
|
263
|
+
.live-card:not(.collapsed) .acc-caret { transform: rotate(90deg); color: var(--accent); }
|
|
256
264
|
.live-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
|
|
257
265
|
.stab-box[hidden], .run-bar[hidden], .run-bar button[hidden] { display: none; }
|
|
258
266
|
.run-bar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; margin: 8px 0 4px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
|
@@ -279,7 +287,7 @@ body.light input[type=checkbox].switch:checked { background: #1a7f37; border-col
|
|
|
279
287
|
.headbar .b { height: 6px; background: var(--panel); border-radius: 3px; position: relative; }
|
|
280
288
|
.headbar .b div { position: absolute; top: 0; bottom: 0; border-radius: 3px; }
|
|
281
289
|
|
|
282
|
-
.settings-wrap { display: grid; grid-template-columns: 205px minmax(0,
|
|
290
|
+
.settings-wrap { display: grid; grid-template-columns: 205px minmax(0, 1fr); gap: 24px; align-items: start; min-width: 0; }
|
|
283
291
|
@media (max-width: 1100px) { .settings-wrap { grid-template-columns: 180px minmax(0, 1fr); gap: 16px; } }
|
|
284
292
|
.snav { display: flex; flex-direction: column; gap: 3px; position: sticky; top: 0; min-width: 0; }
|
|
285
293
|
.snav #s-search { width: 100%; margin-bottom: 10px; background: transparent; border-color: var(--line); padding: 6px 10px; font-size: 13px; }
|
|
@@ -366,6 +374,19 @@ table.cmp td { padding: 3px 8px; }
|
|
|
366
374
|
.brk-alloc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; color: var(--muted); }
|
|
367
375
|
.brk-alloc-legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
368
376
|
.brk-alloc-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
|
|
377
|
+
.brk-specs { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 4px 14px; font-size: 13px; margin: 10px 0; line-height: 1.45; }
|
|
378
|
+
.brk-specs .k { color: var(--muted); }
|
|
379
|
+
.brk-specs .v { color: var(--text); }
|
|
380
|
+
.brk-toolbar { display: flex; flex-wrap: nowrap; align-items: center; gap: 8px; padding: 8px 0 10px; margin-bottom: 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow-x: auto; }
|
|
381
|
+
.brk-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 6px; font-size: 13px; border: 1px solid var(--line); background: var(--panel); color: var(--muted); transition: .15s; }
|
|
382
|
+
.brk-toggle.on { border-color: rgba(22, 163, 74, 0.55); background: rgba(22, 163, 74, 0.14); color: #4ade80; font-weight: 600; }
|
|
383
|
+
.brk-icon-btn { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; font-size: 15px; flex: none; }
|
|
384
|
+
tr.day-group-row td { background: var(--panel); color: var(--text); font-weight: 600; font-size: 12.5px; padding: 7px 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
385
|
+
details.hist-year-box { margin-top: 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); overflow: hidden; }
|
|
386
|
+
details.hist-year-box > summary { padding: 10px 14px; cursor: pointer; font-weight: 600; font-size: 13.5px; background: var(--panel); display: flex; justify-content: space-between; align-items: center; gap: 10px; user-select: none; }
|
|
387
|
+
details.hist-year-box > summary:hover { border-color: var(--accent); color: var(--accent); }
|
|
388
|
+
details.hist-year-box[open] > summary { border-bottom: 1px solid var(--line); }
|
|
389
|
+
@media (max-width: 700px) { .brk-specs { grid-template-columns: 1fr; } }
|
|
369
390
|
|
|
370
391
|
@media (max-width: 900px) {
|
|
371
392
|
body { overflow: auto; }
|
package/server/api/routes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import express from 'express';
|
|
@@ -6,7 +6,7 @@ import { ROOT, strategyDefaults } from '../config.js';
|
|
|
6
6
|
import { activeModelInfo } from '../llm/client.js';
|
|
7
7
|
import { listProviders, getSelected, addProvider, updateProvider, deleteProvider, fetchRemoteModels, testProvider, selectModel, resolveUpstreamForProxy, providerFetch } from '../llm/providers.js';
|
|
8
8
|
import { listRuns, allRuns, getRun, getTrades, deleteRun, updateRunTitle, setRunShowcase, listShowcaseRuns, countUserRunsToday, countUserChatToday, listJournal, deleteJournal, addJournal, listSettingsHistory, kvGet, kvSet } from '../db.js';
|
|
9
|
-
import { PLANS, effectivePlan, publicUser, getUserById, createSession, deleteSession, registerUser, loginUser, ensureNotifyCode, ensureApiKey, getUserByApiKey, activateLocalLicense, listAllUsers, setUserPlan, createPromoCode, listPromoCodes, redeemPromoCode, parseCookies, publicOAuthConfig, saveOAuthConfig, startOAuthRedirect, completeOAuthCallback, verifyTelegramLoginWidget } from '../auth/index.js';
|
|
9
|
+
import { PLANS, effectivePlan, publicUser, getUserById, createSession, deleteSession, registerUser, loginUser, ensureNotifyCode, ensureApiKey, getUserByApiKey, activateLocalLicense, listAllUsers, setUserPlan, createPromoCode, listPromoCodes, redeemPromoCode, acceptUserOffer, createVipRequest, listVipRequests, parseCookies, publicOAuthConfig, saveOAuthConfig, startOAuthRedirect, completeOAuthCallback, verifyTelegramLoginWidget } from '../auth/index.js';
|
|
10
10
|
import { publicBillingConfig, saveBillingConfig, createYookassaPayment, verifyYookassaPayment, verifyUserLatestPayment, listAllPayments } from '../billing/yookassa.js';
|
|
11
11
|
import { publicNotifyConfig, getNotifyConfig, saveNotifyConfig, notifyUser, createTelegramLoginChallenge, checkTelegramLoginChallenge, handleMaxWebhook } from '../notify/index.js';
|
|
12
12
|
import { PARAMS } from '../params-meta.js';
|
|
@@ -129,10 +129,19 @@ export function createRouter() {
|
|
|
129
129
|
res.json({ ok: true, ...sent });
|
|
130
130
|
}));
|
|
131
131
|
r.post('/auth/redeem-promo', wrap((req, res) => {
|
|
132
|
-
if (!req.user) throw new Error('Войдите в аккаунт для активации
|
|
132
|
+
if (!req.user) throw new Error('Войдите в аккаунт для активации кода или VIP-токена');
|
|
133
133
|
const user = redeemPromoCode(req.user.id, req.body?.code);
|
|
134
134
|
res.json({ user });
|
|
135
135
|
}));
|
|
136
|
+
r.post('/auth/accept-offer', wrap((req, res) => {
|
|
137
|
+
if (!req.user) throw new Error('Войдите в аккаунт');
|
|
138
|
+
res.json({ ok: true, user: acceptUserOffer(req.user.id) });
|
|
139
|
+
}));
|
|
140
|
+
r.post('/auth/vip-request', wrap(async (req, res) => {
|
|
141
|
+
const item = createVipRequest({ user: req.user, ...(req.body || {}) });
|
|
142
|
+
notifyUser(1, `👑 Заявка на тариф VIP (НейроАльфа):\nИмя: ${item.name}\nКонтакт: ${item.contact || item.email}\nКапитал: ${item.capitalRub || '—'}\nКомментарий: ${item.comment || '—'}`).catch(() => {});
|
|
143
|
+
res.json({ ok: true, request: item });
|
|
144
|
+
}));
|
|
136
145
|
r.post('/auth/api-key/regenerate', wrap((req, res) => {
|
|
137
146
|
if (!req.user) throw new Error('Войдите в аккаунт');
|
|
138
147
|
const apiKey = ensureApiKey(req.user.id, true);
|
|
@@ -188,49 +197,72 @@ export function createRouter() {
|
|
|
188
197
|
|
|
189
198
|
// ---------- публичная витрина для лендинга ----------
|
|
190
199
|
r.get('/public/showcase', (req, res) => {
|
|
191
|
-
const
|
|
192
|
-
const
|
|
200
|
+
const buildShowcaseItem = (x, full) => {
|
|
201
|
+
const chosen = x.metrics?.model?.chosen || 'attention';
|
|
202
|
+
const cmpFull = x.metrics?.model?.comparison?.[chosen]?.full || full?.extra?.comparison?.[chosen]?.full || {};
|
|
203
|
+
const mFull = { ...(x.metrics?.full || {}), ...cmpFull };
|
|
204
|
+
const p = x.params || {};
|
|
205
|
+
const dir = p.modelDirection || 'long';
|
|
206
|
+
const topK = p.modelTopK || 2;
|
|
207
|
+
const from = x.metrics?.period?.from || 0, till = x.metrics?.period?.till || 0;
|
|
208
|
+
const yrs = Math.max(0.5, (till - from) / (365.25 * 86400000));
|
|
209
|
+
const ret = Number(mFull.returnPct || 0);
|
|
210
|
+
const cagr = yrs >= 0.25 && ret > -99 ? ((Math.pow(1 + ret / 100, 1 / yrs) - 1) * 100) : (ret / yrs);
|
|
211
|
+
|
|
212
|
+
let tierBadge = 'Тариф «Инвестор»';
|
|
213
|
+
let tierColor = 'blue';
|
|
214
|
+
let tierNote = 'Режим «Только в рост (без шорта)» · лимит до 50% годовых · до 1 млн ₽ на счёт';
|
|
215
|
+
let autoTitle = p._title;
|
|
216
|
+
if (dir === 'long' || cagr <= 52) {
|
|
217
|
+
tierBadge = 'Для тарифа «Инвестор»';
|
|
218
|
+
tierColor = 'blue';
|
|
219
|
+
tierNote = 'Торговля только от покупки (без шортов) · потолок тарифа до 50% годовых · до 1 млн ₽/счёт';
|
|
220
|
+
if (!autoTitle) autoTitle = 'Консервативный рост · Только покупки';
|
|
221
|
+
} else if (cagr <= 105) {
|
|
222
|
+
tierBadge = 'Для тарифа «Профи»';
|
|
223
|
+
tierColor = 'indigo';
|
|
224
|
+
tierNote = 'Заработок на росте и падении акций (с шортами) · потолок тарифа до 100% годовых · до 10 млн ₽/счёт';
|
|
225
|
+
if (!autoTitle) autoTitle = 'Активный рост и падение · С шортами';
|
|
226
|
+
} else {
|
|
227
|
+
tierBadge = '👑 Для тарифа «VIP»';
|
|
228
|
+
tierColor = 'gold';
|
|
229
|
+
tierNote = 'Полная мощность без потолка доходности · форсаж H/2 + концентрация слота Conviction Sizing';
|
|
230
|
+
if (!autoTitle || autoTitle === 'Эталонная адаптивная стратегия') autoTitle = 'Институциональный форсаж · Полная мощность';
|
|
231
|
+
}
|
|
193
232
|
return {
|
|
194
233
|
id: x.id,
|
|
195
|
-
title:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
title: autoTitle,
|
|
235
|
+
tierBadge,
|
|
236
|
+
tierColor,
|
|
237
|
+
tierNote,
|
|
238
|
+
params: { modelHorizon: p.modelHorizon || 2.5, modelTopK: topK, modelDirection: dir, modelType: chosen },
|
|
239
|
+
metrics: mFull,
|
|
240
|
+
oos: x.metrics?.model?.comparison?.[chosen]?.outOfSample || full?.extra?.comparison?.[chosen]?.outOfSample || {},
|
|
199
241
|
period: x.metrics?.period || {},
|
|
200
|
-
curve: full?.extra?.curves?.[
|
|
242
|
+
curve: full?.extra?.curves?.[chosen]?.slice(-180) || [],
|
|
201
243
|
marketCurve: full?.extra?.curves?.market?.slice(-180) || [],
|
|
202
244
|
};
|
|
203
|
-
}
|
|
245
|
+
};
|
|
246
|
+
const runs = listShowcaseRuns().map((x) => buildShowcaseItem(x, getRun(x.id)));
|
|
204
247
|
if (!runs.length) {
|
|
205
248
|
try {
|
|
206
249
|
const ref = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'seed-run.json'), 'utf8'));
|
|
207
|
-
|
|
208
|
-
if (att?.full) {
|
|
209
|
-
runs.push({
|
|
210
|
-
id: 1,
|
|
211
|
-
title: ref.params?._title || 'Эталонная адаптивная стратегия «Внимание через карту»',
|
|
212
|
-
params: { modelHorizon: ref.params?.modelHorizon || 2, modelTopK: ref.params?.modelTopK || 3, modelDirection: ref.params?.modelDirection || 'longshort', modelType: 'attention' },
|
|
213
|
-
metrics: att.full,
|
|
214
|
-
oos: att.outOfSample || {},
|
|
215
|
-
period: ref.metrics?.period || {},
|
|
216
|
-
curve: (ref.extra?.curves?.attention || []).slice(-180),
|
|
217
|
-
marketCurve: (ref.extra?.curves?.market || []).slice(-180),
|
|
218
|
-
});
|
|
219
|
-
}
|
|
250
|
+
if (ref) runs.push(buildShowcaseItem({ id: 1, ...ref }, ref));
|
|
220
251
|
} catch { /* ignore */ }
|
|
221
252
|
}
|
|
222
253
|
res.json({ runs, plans: PLANS, legal: getLegalInfo() });
|
|
223
254
|
});
|
|
224
255
|
|
|
225
|
-
// ---------- реквизиты оферты (для модерации ЮKassa) ----------
|
|
256
|
+
// ---------- реквизиты оферты (для модерации ЮKassa и правовых документов) ----------
|
|
226
257
|
const DEFAULT_LEGAL = {
|
|
227
|
-
ownerName: '
|
|
228
|
-
|
|
258
|
+
ownerName: 'ООО «БИС» («Бизнес Интер Софт»)',
|
|
259
|
+
authorName: 'Перепелкин Роман Александрович',
|
|
260
|
+
status: 'Оператор платформы PaaS · Проприетарное ПО (Все права защищены)',
|
|
229
261
|
inn: '',
|
|
230
262
|
ogrn: '',
|
|
231
263
|
email: 'support@neuroalpha.ru',
|
|
232
264
|
phone: '',
|
|
233
|
-
address: 'Российская
|
|
265
|
+
address: 'Российская Федерация, г. Рязань',
|
|
234
266
|
};
|
|
235
267
|
const getLegalInfo = () => ({ ...DEFAULT_LEGAL, ...(kvGet('legalInfo') || {}) });
|
|
236
268
|
r.get('/public/legal', (req, res) => res.json(getLegalInfo()));
|
|
@@ -241,7 +273,7 @@ export function createRouter() {
|
|
|
241
273
|
};
|
|
242
274
|
r.get('/admin/state', wrap((req, res) => {
|
|
243
275
|
requireAdmin(req);
|
|
244
|
-
res.json({ users: listAllUsers(), payments: listAllPayments(), promos: listPromoCodes(), billing: publicBillingConfig(), notify: publicNotifyConfig(), oauth: publicOAuthConfig(), legal: getLegalInfo() });
|
|
276
|
+
res.json({ users: listAllUsers(), payments: listAllPayments(), promos: listPromoCodes(), vipRequests: listVipRequests(), billing: publicBillingConfig(), notify: publicNotifyConfig(), oauth: publicOAuthConfig(), legal: getLegalInfo() });
|
|
245
277
|
}));
|
|
246
278
|
r.post('/admin/oauth', wrap((req, res) => {
|
|
247
279
|
requireAdmin(req);
|
package/server/auth/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -74,11 +74,61 @@ export function verifyPassword(password, stored) {
|
|
|
74
74
|
return hash.length === target.length && crypto.timingSafeEqual(hash, target);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// ----------
|
|
77
|
+
// ---------- пользователи, тарифные планы (4 уровня) и сессии ----------
|
|
78
78
|
export const PLANS = {
|
|
79
|
-
free: {
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
free: {
|
|
80
|
+
id: 'free',
|
|
81
|
+
name: 'Старт (Тест-драйв)',
|
|
82
|
+
priceRub: 0,
|
|
83
|
+
maxRunsPerDay: 20,
|
|
84
|
+
maxVirtualAccounts: 1,
|
|
85
|
+
maxBrokerAccounts: 0,
|
|
86
|
+
maxCapitalPerAccountRub: 0,
|
|
87
|
+
allowShorts: false,
|
|
88
|
+
allowVipBoost: false,
|
|
89
|
+
maxAnnualReturnPct: 50,
|
|
90
|
+
maxChatPerDay: 20,
|
|
91
|
+
},
|
|
92
|
+
investor: {
|
|
93
|
+
id: 'investor',
|
|
94
|
+
name: 'Инвестор',
|
|
95
|
+
priceRub: 3900,
|
|
96
|
+
maxRunsPerDay: 50,
|
|
97
|
+
maxVirtualAccounts: 3,
|
|
98
|
+
maxBrokerAccounts: 2,
|
|
99
|
+
maxCapitalPerAccountRub: 1000000,
|
|
100
|
+
allowShorts: false,
|
|
101
|
+
allowVipBoost: false,
|
|
102
|
+
maxAnnualReturnPct: 50,
|
|
103
|
+
maxChatPerDay: 200,
|
|
104
|
+
},
|
|
105
|
+
pro: {
|
|
106
|
+
id: 'pro',
|
|
107
|
+
name: 'Профи',
|
|
108
|
+
priceRub: 9900,
|
|
109
|
+
maxRunsPerDay: 500,
|
|
110
|
+
maxVirtualAccounts: 5,
|
|
111
|
+
maxBrokerAccounts: 5,
|
|
112
|
+
maxCapitalPerAccountRub: 10000000,
|
|
113
|
+
allowShorts: true,
|
|
114
|
+
allowVipBoost: false,
|
|
115
|
+
maxAnnualReturnPct: 100,
|
|
116
|
+
maxChatPerDay: 1000,
|
|
117
|
+
},
|
|
118
|
+
vip: {
|
|
119
|
+
id: 'vip',
|
|
120
|
+
name: 'VIP (Без ограничений)',
|
|
121
|
+
priceRub: 0, // по индивидуальному согласованию / VIP-токену от администратора
|
|
122
|
+
customPrice: true,
|
|
123
|
+
maxRunsPerDay: 10000,
|
|
124
|
+
maxVirtualAccounts: 20,
|
|
125
|
+
maxBrokerAccounts: 50,
|
|
126
|
+
maxCapitalPerAccountRub: 1000000000,
|
|
127
|
+
allowShorts: true,
|
|
128
|
+
allowVipBoost: true,
|
|
129
|
+
maxAnnualReturnPct: 0, // 0 = без потолка доходности (полный форсаж H/2 + Conviction Sizing)
|
|
130
|
+
maxChatPerDay: 5000,
|
|
131
|
+
},
|
|
82
132
|
};
|
|
83
133
|
|
|
84
134
|
export function generateApiKey() {
|
|
@@ -158,18 +208,22 @@ async function patchUserLicense(cleanKey, baseUrl) {
|
|
|
158
208
|
|
|
159
209
|
export function effectivePlan(user) {
|
|
160
210
|
if (!user) return PLANS.free;
|
|
161
|
-
// В локальном
|
|
162
|
-
// если
|
|
211
|
+
// В локальном режиме: если активирован лицензионный ключ — действует тариф подписки;
|
|
212
|
+
// если это явно админ — действует VIP-тариф без ограничений
|
|
163
213
|
if (process.env.CLOUD_MODE !== '1' && user.id === 1) {
|
|
214
|
+
if (user.plan && PLANS[user.plan] && user.plan !== 'pro') {
|
|
215
|
+
if (user.plan !== 'free' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.free;
|
|
216
|
+
return PLANS[user.plan];
|
|
217
|
+
}
|
|
164
218
|
const lic = getLocalLicense();
|
|
165
219
|
if (lic?.plan && PLANS[lic.plan]) {
|
|
166
220
|
if (lic.plan !== 'free' && lic.planUntil > 0 && lic.planUntil < Date.now()) return PLANS.free;
|
|
167
221
|
return PLANS[lic.plan];
|
|
168
222
|
}
|
|
169
223
|
if (process.env.LOCAL_STRICT_LICENSE === '1') return PLANS.free;
|
|
170
|
-
return PLANS.
|
|
224
|
+
return PLANS.vip;
|
|
171
225
|
}
|
|
172
|
-
if (user.role === 'admin') return PLANS.
|
|
226
|
+
if (user.role === 'admin') return PLANS[user.plan] || PLANS.vip;
|
|
173
227
|
const p = PLANS[user.plan] || PLANS.free;
|
|
174
228
|
if (p.id !== 'free' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.free;
|
|
175
229
|
return p;
|
|
@@ -194,11 +248,19 @@ export function publicUser(u) {
|
|
|
194
248
|
tgConnected: Boolean(u.tg_chat_id),
|
|
195
249
|
maxConnected: Boolean(u.max_chat_id),
|
|
196
250
|
notifyCode: u.notify_code || null,
|
|
251
|
+
offerAcceptedAt: u.offer_accepted_at || 0,
|
|
197
252
|
localLicense: process.env.CLOUD_MODE !== '1' ? getLocalLicense() : null,
|
|
198
253
|
createdAt: u.created_at,
|
|
199
254
|
};
|
|
200
255
|
}
|
|
201
256
|
|
|
257
|
+
export function acceptUserOffer(userId) {
|
|
258
|
+
if (!userId) throw new Error('Войдите в аккаунт для подписания оферты');
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
db.prepare('UPDATE users SET offer_accepted_at = ? WHERE id = ?').run(now, Number(userId));
|
|
261
|
+
return publicUser(getUserById(userId));
|
|
262
|
+
}
|
|
263
|
+
|
|
202
264
|
export function getUserById(id) {
|
|
203
265
|
return db.prepare('SELECT * FROM users WHERE id = ?').get(Number(id));
|
|
204
266
|
}
|
|
@@ -305,13 +367,16 @@ export function setUserPlan(userId, plan, days = 30) {
|
|
|
305
367
|
return publicUser(getUserById(userId));
|
|
306
368
|
}
|
|
307
369
|
|
|
308
|
-
// ---------- промокоды ----------
|
|
309
|
-
export function createPromoCode({ code, plan = 'investor', days = 14, maxUses = 50 }) {
|
|
310
|
-
|
|
370
|
+
// ---------- промокоды и персональные VIP-токены ----------
|
|
371
|
+
export function createPromoCode({ code, plan = 'investor', days = 14, maxUses = 50, generateVip = false }) {
|
|
372
|
+
let clean = String(code || '').trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '');
|
|
373
|
+
if (generateVip || (!clean && plan === 'vip')) {
|
|
374
|
+
clean = `VIP-${crypto.randomBytes(3).toString('hex').toUpperCase()}-${crypto.randomBytes(2).toString('hex').toUpperCase()}`;
|
|
375
|
+
}
|
|
311
376
|
if (clean.length < 3) throw new Error('Код должен содержать минимум 3 символа (латиница/цифры)');
|
|
312
|
-
if (!(plan in PLANS) || plan === 'free') throw new Error('Выберите тариф
|
|
313
|
-
const d = Math.max(1, Math.min(
|
|
314
|
-
const m = Math.max(1, Math.min(10000, Math.round(Number(maxUses) || 50)));
|
|
377
|
+
if (!(plan in PLANS) || plan === 'free') throw new Error('Выберите тариф Инвестор, Профи или VIP');
|
|
378
|
+
const d = Math.max(1, Math.min(3650, Math.round(Number(days) || (plan === 'vip' ? 365 : 14))));
|
|
379
|
+
const m = Math.max(1, Math.min(10000, Math.round(Number(maxUses) || (plan === 'vip' ? 1 : 50))));
|
|
315
380
|
db.prepare(`INSERT OR REPLACE INTO promocodes (code, plan, days, max_uses, used_count, created_at)
|
|
316
381
|
VALUES (?, ?, ?, ?, COALESCE((SELECT used_count FROM promocodes WHERE code = ?), 0), ?)`).run(clean, plan, d, m, clean, Date.now());
|
|
317
382
|
return listPromoCodes();
|
|
@@ -322,22 +387,46 @@ export function listPromoCodes() {
|
|
|
322
387
|
}
|
|
323
388
|
|
|
324
389
|
export function redeemPromoCode(userId, rawCode) {
|
|
325
|
-
if (!userId) throw new Error('Войдите в аккаунт для активации
|
|
390
|
+
if (!userId) throw new Error('Войдите в аккаунт для активации кода или VIP-токена');
|
|
326
391
|
const clean = String(rawCode || '').trim().toUpperCase();
|
|
327
392
|
const promo = db.prepare('SELECT * FROM promocodes WHERE UPPER(code) = ?').get(clean);
|
|
328
|
-
if (!promo) throw new Error('Промокод не найден');
|
|
329
|
-
if (promo.used_count >= promo.max_uses) throw new Error('Лимит активаций этого
|
|
393
|
+
if (!promo) throw new Error('Промокод или VIP-токен не найден');
|
|
394
|
+
if (promo.used_count >= promo.max_uses) throw new Error('Лимит активаций этого кода или VIP-токена исчерпан');
|
|
330
395
|
const already = db.prepare('SELECT 1 FROM promocode_uses WHERE code = ? AND user_id = ?').get(promo.code, Number(userId));
|
|
331
|
-
if (already) throw new Error('Вы уже активировали этот
|
|
396
|
+
if (already) throw new Error('Вы уже активировали этот код');
|
|
332
397
|
const u = getUserById(userId);
|
|
333
398
|
const baseTs = u.plan === promo.plan && u.plan_until > Date.now() ? u.plan_until : Date.now();
|
|
334
399
|
const nextUntil = baseTs + promo.days * 86400000;
|
|
335
|
-
db.prepare('UPDATE users SET plan = ?, plan_until = ? WHERE id = ?').run(promo.plan, nextUntil, u.id);
|
|
400
|
+
db.prepare('UPDATE users SET plan = ?, plan_until = ?, offer_accepted_at = COALESCE(NULLIF(offer_accepted_at, 0), ?) WHERE id = ?').run(promo.plan, nextUntil, Date.now(), u.id);
|
|
336
401
|
db.prepare('UPDATE promocodes SET used_count = used_count + 1 WHERE code = ?').run(promo.code);
|
|
337
402
|
db.prepare('INSERT INTO promocode_uses (code, user_id, used_at) VALUES (?, ?, ?)').run(promo.code, u.id, Date.now());
|
|
338
403
|
return publicUser(getUserById(userId));
|
|
339
404
|
}
|
|
340
405
|
|
|
406
|
+
export function createVipRequest({ user, name, contact, capitalRub, comment }) {
|
|
407
|
+
const list = kvGet('vipRequests') || [];
|
|
408
|
+
const item = {
|
|
409
|
+
id: `vipreq_${Date.now()}`,
|
|
410
|
+
createdAt: Date.now(),
|
|
411
|
+
userId: user?.id || null,
|
|
412
|
+
email: user?.email || '',
|
|
413
|
+
name: String(name || user?.name || '').trim() || 'Инвестор',
|
|
414
|
+
contact: String(contact || '').trim(),
|
|
415
|
+
capitalRub: String(capitalRub || '').trim(),
|
|
416
|
+
comment: String(comment || '').trim(),
|
|
417
|
+
status: 'new',
|
|
418
|
+
};
|
|
419
|
+
if (!item.contact && !item.email) throw new Error('Укажите ваш Telegram, телефон или email для связи');
|
|
420
|
+
list.unshift(item);
|
|
421
|
+
if (list.length > 200) list.length = 200;
|
|
422
|
+
kvSet('vipRequests', list);
|
|
423
|
+
return item;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function listVipRequests() {
|
|
427
|
+
return kvGet('vipRequests') || [];
|
|
428
|
+
}
|
|
429
|
+
|
|
341
430
|
export function authMiddleware(req, res, next) {
|
|
342
431
|
const u = userFromRequest(req);
|
|
343
432
|
req.user = u;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import { db, kvGet, kvSet } from '../db.js';
|
|
4
4
|
import { PLANS, encryptSecret, decryptSecret, getUserById, publicUser } from '../auth/index.js';
|