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.
Files changed (41) hide show
  1. package/LICENSE +13 -17
  2. package/README.md +4 -2
  3. package/bin/cli.js +1 -1
  4. package/deploy/install.sh +1 -1
  5. 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
  6. 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
  7. package/package.json +2 -2
  8. package/public/admin.html +43 -20
  9. package/public/app.js +524 -211
  10. package/public/index.html +43 -3
  11. package/public/landing.html +328 -115
  12. package/public/legal.html +37 -16
  13. package/public/styles.css +24 -3
  14. package/server/api/routes.js +61 -29
  15. package/server/auth/index.js +109 -20
  16. package/server/billing/yookassa.js +1 -1
  17. package/server/brokers/index.js +179 -34
  18. package/server/config.js +1 -1
  19. package/server/data/moex.js +1 -1
  20. package/server/db.js +17 -3
  21. package/server/hub.js +1 -1
  22. package/server/index.js +4 -4
  23. package/server/llm/client.js +1 -1
  24. package/server/llm/providers.js +1 -1
  25. package/server/model/dataset.js +1 -1
  26. package/server/model/forecast.js +188 -17
  27. package/server/model/linalg.js +1 -1
  28. package/server/model/live.js +6 -19
  29. package/server/model/metrics.js +1 -1
  30. package/server/model/models.js +1 -1
  31. package/server/model/profiles.js +1 -1
  32. package/server/model/research.js +12 -36
  33. package/server/model/vocab.js +1 -1
  34. package/server/news/index.js +1 -1
  35. package/server/notify/index.js +1 -1
  36. package/server/params-meta.js +4 -3
  37. package/server/settings.js +1 -1
  38. package/server/strategist/analytics.js +2 -2
  39. package/server/strategist/index.js +2 -2
  40. package/server/strategist/tools.js +1 -1
  41. package/server/util.js +1 -1
@@ -1,4 +1,4 @@
1
- <!-- Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License. -->
1
+ <!-- Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software. -->
2
2
  <!doctype html>
3
3
  <html lang="ru">
4
4
  <head>
@@ -210,7 +210,8 @@
210
210
  .bcard p { margin: 0; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
211
211
 
212
212
  /* ---------- Живая витрина стратегий ---------- */
213
- .showcase-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 22px; }
213
+ .showcase-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; align-items: stretch; }
214
+ @media (max-width: 960px) { .showcase-grid { grid-template-columns: 1fr; } }
214
215
  .sc-card {
215
216
  background: var(--panel); border: 1px solid var(--line);
216
217
  border-radius: 18px; padding: 24px;
@@ -231,7 +232,8 @@
231
232
  padding: 8px 18px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 14px; transition: .15s;
232
233
  }
233
234
  .period-seg button.active { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff; }
234
- .plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 24px; align-items: stretch; }
235
+ .plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; align-items: stretch; }
236
+ @media (max-width: 960px) { .plans { grid-template-columns: 1fr; } }
235
237
  .plan {
236
238
  background: var(--panel); border: 1px solid var(--line);
237
239
  border-radius: 18px; padding: 30px 26px;
@@ -244,19 +246,32 @@
244
246
  border-color: rgba(96, 165, 250, 0.55);
245
247
  box-shadow: 0 20px 50px -12px rgba(37, 99, 235, 0.35);
246
248
  }
249
+ .plan-vip-wide {
250
+ margin-top: 22px;
251
+ background: linear-gradient(135deg, rgba(120, 53, 15, 0.22), rgba(17, 24, 39, 0.94));
252
+ border: 1px solid rgba(245, 158, 11, 0.5);
253
+ border-radius: 18px; padding: 28px 32px;
254
+ display: grid; grid-template-columns: 1fr 1.3fr auto; gap: 28px; align-items: center;
255
+ position: relative;
256
+ box-shadow: 0 20px 50px -15px rgba(217, 119, 6, 0.25);
257
+ }
258
+ @media (max-width: 960px) { .plan-vip-wide { grid-template-columns: 1fr; } }
247
259
  .pop-ribbon {
248
260
  position: absolute; top: 14px; right: 16px;
249
261
  background: linear-gradient(135deg, var(--accent), var(--accent2));
250
262
  color: #fff; font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px;
251
263
  text-transform: uppercase; letter-spacing: 0.5px;
252
264
  }
253
- .plan h3 { margin: 0 0 4px; font-size: 21px; }
265
+ .plan h3, .plan-vip-wide h3 { margin: 0 0 4px; font-size: 21px; }
254
266
  .plan-desc { font-size: 13.5px; color: var(--muted); margin-bottom: 14px; }
255
267
  .price { font-size: 38px; font-weight: 800; margin: 6px 0 18px; letter-spacing: -1px; }
256
268
  .price span { font-size: 14px; font-weight: 400; color: var(--muted); letter-spacing: 0; }
257
- .plan ul { list-style: none; padding: 0; margin: 0 0 26px; font-size: 14.5px; color: #cbd5e1; flex: 1; }
258
- .plan li { margin: 10px 0; display: flex; gap: 10px; align-items: flex-start; }
259
- .plan li::before { content: "✓"; color: var(--green); font-weight: 800; }
269
+ .plan ul, .plan-vip-wide ul { list-style: none; padding: 0; margin: 0 0 26px; font-size: 14.5px; color: #cbd5e1; flex: 1; }
270
+ .plan-vip-wide ul { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; }
271
+ @media (max-width: 720px) { .plan-vip-wide ul { grid-template-columns: 1fr; } }
272
+ .plan li, .plan-vip-wide li { margin: 10px 0; position: relative; padding-left: 22px; line-height: 1.45; }
273
+ .plan-vip-wide li { margin: 0; }
274
+ .plan li::before, .plan-vip-wide li::before { content: "✓"; color: var(--green); font-weight: 800; position: absolute; left: 0; top: 0; }
260
275
 
261
276
  /* ---------- Анимация появления при прокрутке ---------- */
262
277
  .reveal { opacity: 0; transform: translateY(22px); transition: opacity .6s ease, transform .6s ease; }
@@ -314,13 +329,13 @@
314
329
  <nav class="nav-links">
315
330
  <a href="#showcase">Результаты</a>
316
331
  <a href="#how">Технология</a>
317
- <a href="#cli-sec">Запуск npx / API</a>
332
+ <a href="#paas-sec">Как работает PaaS</a>
318
333
  <a href="#brokers">Брокеры и мессенджеры</a>
319
334
  <a href="#pricing">Тарифы</a>
320
335
  </nav>
321
336
  <div style="display:flex;gap:10px" id="nav-auth">
322
337
  <button class="btn" id="btn-login-open">Войти</button>
323
- <a class="btn primary" href="#cli-sec" id="btn-nav-cta">Установить npx →</a>
338
+ <a class="btn primary" href="#pricing" id="btn-nav-cta">Открыть терминал →</a>
324
339
  </div>
325
340
  </div>
326
341
  </header>
@@ -351,8 +366,8 @@
351
366
  <h1><span class="grad-text">ИИ-автопилот,</span> который читает рынок акций как единый язык</h1>
352
367
  <p class="hero-sub">Платформа находит опережающие волны капитала между связанными бумагами, сама адаптирует пороги под комиссию вашего брокера и присылает готовые заявки в <b>Telegram и MAX</b> с подтверждением в 1 клик.</p>
353
368
  <div class="hero-actions">
354
- <button class="btn primary lg" id="btn-hero-start">Получить API-ключ</button>
355
- <a class="btn lg" href="#cli-sec">Запуск одной командой npx →</a>
369
+ <button class="btn primary lg" id="btn-hero-start">Попробовать бесплатно</button>
370
+ <a class="btn lg" href="#pricing">Выбрать тариф PaaS →</a>
356
371
  </div>
357
372
  <div class="mini-stats">
358
373
  <div class="mini-stat"><div class="num pos">+95% … +422%</div><div class="lbl">на истории 2024–2026 против −23,6% у индекса Мосбиржи</div></div>
@@ -455,26 +470,27 @@
455
470
  </div>
456
471
  </section>
457
472
 
458
- <section id="cli-sec" class="reveal">
473
+ <section id="paas-sec" class="reveal">
459
474
  <div class="sec-head">
460
- <div class="sec-tag">Полная приватность · Установка к себе за 10 секунд</div>
461
- <h2>Запуск на вашем компьютере через <code>npx</code> с готовым ИИ «Стратегом»</h2>
462
- <p class="sec-sub">Терминал работает локально на вашем ПК или сервере (ваши брокерские токены никогда не передаются в интернет), а модель ИИ <code>Qwen3.8 27b</code> и подписка подключаются автоматически через ваш персональный <code>APIKey</code>.</p>
475
+ <div class="sec-tag">Облачная PaaS-инфраструктура · Без установки на компьютер</div>
476
+ <h2>От исследования стратегии до реальных заявок за 1 минуту</h2>
477
+ <p class="sec-sub">Вся вычислительная мощность и ИИ-управляющий «Стратег» работают в защищённом облаке 24/7. Вы управляете счетами прямо из браузера или со смартфона в Telegram и MAX.</p>
463
478
  </div>
464
479
  <div class="bento">
465
- <div class="bcard wide">
480
+ <div class="bcard">
466
481
  <div class="bicon">1</div>
467
- <h3>Зарегистрируйтесь и получите APIKey</h3>
468
- <p>Войдите через Telegram или Email и выберите тариф. В личном кабинете сразу появится ваш ключ <code>na_live_…</code>, который включает подписку и встроенного ИИ-Стратега.</p>
482
+ <h3>Тестируйте без ограничений</h3>
483
+ <p>Даже на бесплатном тарифе вы можете проводить эксперименты на полной мощности ядра, проверять стратегии на сдвигах истории и видеть весь потенциал доходности.</p>
469
484
  </div>
470
- <div class="bcard wide">
485
+ <div class="bcard">
471
486
  <div class="bicon">2</div>
472
- <h3>Выполните одну команду в терминале</h3>
473
- <p style="margin-bottom:14px">На Windows, macOS или Linux (нужен Node.js ≥ 22.13) выполните команду — браузер с терминалом откроется сам:</p>
474
- <div class="cli-box">
475
- <code id="landing-cli-cmd">npx neuroalpha --key na_live_ваш_ключ</code>
476
- <button type="button" class="btn" id="btn-copy-landing-cli" style="padding:7px 14px;font-size:13px">Скопировать</button>
477
- </div>
487
+ <h3>Выберите мощность тарифа</h3>
488
+ <p>От консервативного тарифа «Инвестор» (работа только в рост до 50% годовых) и «Профи» (покупки + шорты до 100% годовых) до полного форсажа «VIP» без потолка доходности.</p>
489
+ </div>
490
+ <div class="bcard">
491
+ <div class="bicon">3</div>
492
+ <h3>Торгуйте с жёсткими предохранителями</h3>
493
+ <p>Подключите счёт Т-Банка, БКС, Финам или Алор в режиме «Подтверждение в 1 клик в Telegram / MAX» или «Полный автомат» с защитой стартового портфеля.</p>
478
494
  </div>
479
495
  </div>
480
496
  </section>
@@ -519,58 +535,78 @@
519
535
  </div>
520
536
  <div class="plans">
521
537
  <div class="plan">
522
- <h3>Старт (Витрина)</h3>
523
- <div class="plan-desc">Для знакомства с платформой и проверки сигналов на демо-счёте</div>
524
- <div class="price">0 ₽ <span>/ навсегда</span></div>
538
+ <h3>Старт (Тест-драйв)</h3>
539
+ <div class="plan-desc">Для тестирования любых стратегий и проверки сигналов на виртуальном счёте без реальной торговли</div>
540
+ <div class="price">0 ₽ <span>/ бесплатно</span></div>
525
541
  <ul>
526
- <li>Доступ к витрине эталонных стратегий и карте рынка</li>
527
- <li>Свежие новости Мосбиржи с ИИ-разметкой тональности</li>
528
- <li>1 виртуальный счёт в реальном времени со сверкой коридора</li>
529
- <li>До 2 собственных исторических исследований в сутки</li>
542
+ <li><b>Полный доступ к Экспериментам</b>: тестируйте стратегии любой доходности</li>
543
+ <li>1 виртуальный демо-счёт в реальном времени со сверкой коридора</li>
544
+ <li>Карта рынка, новости Мосбиржи и ИИ-управляющий «Стратег»</li>
545
+ <li>Без подключения реальных брокерских счетов</li>
530
546
  </ul>
531
- <button class="btn" type="button" id="btn-free-start">Получить бесплатный ключ</button>
547
+ <button class="btn" type="button" id="btn-free-start">Начать тестирование</button>
532
548
  </div>
533
549
 
534
- <div class="plan featured">
535
- <div class="pop-ribbon">Выбор инвесторов</div>
536
- <h3>Инвестор (Автопилот)</h3>
537
- <div class="plan-desc">Для активного управления своим счётом с подтверждением в Telegram и MAX</div>
550
+ <div class="plan">
551
+ <h3>Инвестор</h3>
552
+ <div class="plan-desc">Консервативная реальная торговля только от покупки (без шортов)</div>
538
553
  <div class="price" id="price-investor">3 900 ₽ <span>/ мес</span></div>
539
554
  <ul>
540
- <li>Подключение до 2 брокерских счетов (Т-Банк, БКС, Финам, Алор)</li>
541
- <li>Авто-расписание заявок и кнопки подтверждения в <b>Telegram и MAX</b></li>
542
- <li>Самообучение правил торговли и стресс-тест устойчивости</li>
543
- <li>До 3 независимых виртуальных счетов и 30 экспериментов в сутки</li>
544
- <li>Голосовой и текстовый ИИ-управляющий «Стратег»</li>
555
+ <li>До <b>2 брокерских счетов</b> (Т-Банк, БКС, Финам, Алор)</li>
556
+ <li>Лимит капитала в торговле: <b>до 1 000 000 ₽</b> на каждом счёте</li>
557
+ <li>Режим <b>«Только в рост (без шорта)»</b>, консервативный слот</li>
558
+ <li>Предельный уровень доходности стратегии: <b>до 50% годовых</b></li>
559
+ <li>Подтверждение заявок в 1 клик в <b>Telegram и MAX</b></li>
545
560
  </ul>
546
- <button class="btn primary" data-buy="investor">Оформить подписку</button>
561
+ <button class="btn primary" data-buy="investor">Оформить «Инвестор»</button>
547
562
  </div>
548
563
 
549
- <div class="plan">
550
- <h3>Профи / Лаборатория</h3>
551
- <div class="plan-desc">Максимальная мощность, нейросеть-трансформер и до 5 счетов</div>
564
+ <div class="plan featured">
565
+ <div class="pop-ribbon">Оптимальный выбор</div>
566
+ <h3>Профи</h3>
567
+ <div class="plan-desc">Заработок и на росте, и на падении акций (с шортами)</div>
552
568
  <div class="price" id="price-pro">9 900 ₽ <span>/ мес</span></div>
553
569
  <ul>
554
- <li>До 5 брокерских и 5 виртуальных счетов параллельно</li>
555
- <li>Обучение нейросети-трансформера и свои подключения к моделям ИИ</li>
556
- <li>До 500 экспериментов в сутки, приоритет в очереди расчётов</li>
557
- <li>Все функции автопилота, предохранителей и ботов Telegram / MAX</li>
570
+ <li>До <b>5 брокерских счетов</b> параллельно</li>
571
+ <li>Лимит капитала в торговле: <b>до 10 000 000 ₽</b> на каждом счёте</li>
572
+ <li>Режим <b>«В рост и в падение (с шортом)»</b> с адаптацией к индексу</li>
573
+ <li>Предельный уровень доходности стратегии: <b>до 100% годовых</b></li>
574
+ <li>До 5 виртуальных счетов и 500 экспериментов в сутки</li>
558
575
  </ul>
559
- <button class="btn primary" data-buy="pro">Оформить подписку</button>
576
+ <button class="btn primary" data-buy="pro">Оформить «Профи»</button>
577
+ </div>
578
+ </div>
579
+
580
+ <div class="plan-vip-wide">
581
+ <div>
582
+ <span class="pop-ribbon" style="position:static;display:inline-block;margin-bottom:8px;background:linear-gradient(135deg,#d97706,#f59e0b)">Полная институциональная мощность</span>
583
+ <h3 style="font-size:24px;margin-bottom:6px">👑 Тариф VIP (Без ограничений)</h3>
584
+ <div class="plan-desc" style="margin-bottom:10px">Индивидуальный контур без ограничений по доходности, числу счетов и объёму капитала</div>
585
+ <div style="font-size:26px;font-weight:800;color:#fbbf24">По согласованию</div>
586
+ </div>
587
+ <ul>
588
+ <li><b>Без ограничений</b> по числу брокерских счетов и сумме капитала на каждом счёте</li>
589
+ <li><b>Без потолка годовой доходности</b> (100% мощности лучших экспериментов)</li>
590
+ <li>Включён <b>VIP-форсаж</b>: внутричасовой перехват <code>H/2</code> + динамическая концентрация <code>Conviction Sizing</code></li>
591
+ <li>Персональный VIP-токен активации от администратора платформы</li>
592
+ </ul>
593
+ <div style="display:flex;flex-direction:column;gap:10px;min-width:220px">
594
+ <button class="btn primary" type="button" id="btn-vip-req" style="background:linear-gradient(135deg,#d97706,#f59e0b);justify-content:center">Согласовать условия VIP</button>
595
+ <button class="btn" type="button" id="btn-vip-token-open" style="font-size:13px;padding:9px 14px;justify-content:center">У меня есть VIP-токен</button>
560
596
  </div>
561
597
  </div>
562
598
  </section>
563
599
 
564
600
  <footer>
565
601
  <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px">
566
- <div>© 2026 Roman A. Perepelkin · НейроАльфа · Платформа алгоритмического анализа рынка акций (MIT License)</div>
602
+ <div>© 2026 Автор и правообладатель: Перепелкин Р.А. · Оператор платформы: ООО «БИС» («Бизнес Интер Софт», г. Рязань) · Все права защищены</div>
567
603
  <div style="display:flex;gap:18px">
568
604
  <a href="/legal#offer" style="color:#93c5fd">Публичная оферта</a>
569
605
  <a href="/legal#privacy" style="color:#93c5fd">Политика конфиденциальности</a>
570
606
  <a href="/legal#risks" style="color:#93c5fd">Декларация о рисках</a>
571
607
  </div>
572
608
  </div>
573
- <div style="margin-top:8px">Сервис является аналитическим программным обеспечением (SaaS) и не является индивидуальной инвестиционной рекомендацией или доверительным управлением. Результаты на исторических данных не гарантируют доходности в будущем.</div>
609
+ <div style="margin-top:8px">Проприетарное программное обеспечение (PaaS). Сервис является аналитическим программно-вычислительным комплексом и не является индивидуальной инвестиционной рекомендацией или доверительным управлением. Результаты на исторических данных не гарантируют доходности в будущем.</div>
574
610
  </footer>
575
611
  </div>
576
612
 
@@ -625,6 +661,64 @@
625
661
  </div>
626
662
  </div>
627
663
 
664
+ <!-- Модальное окно подписания Договора-оферты и Декларации о рисках перед подпиской -->
665
+ <div class="modal" id="offer-modal" hidden>
666
+ <div class="modal-box" style="max-width:660px">
667
+ <h3>Договор публичной оферты и Декларация о рисках</h3>
668
+ <div style="background:#070b14;border:1px solid var(--line);border-radius:10px;padding:14px 16px;max-height:260px;overflow-y:auto;font-size:13px;line-height:1.55;color:#cbd5e1;margin-bottom:14px">
669
+ <p style="margin-top:0"><b>1. Предмет договора:</b> Настоящая Публичная оферта регулирует предоставление доступа к облачной аналитической платформе «НейроАльфа» (PaaS/SaaS) согласно выбранному тарифному плану («Старт», «Инвестор», «Профи», «VIP»).</p>
670
+ <p><b>2. Правовой статус (отсутствие ИИР и ДУ):</b> Сервис является исключительно программно-техническим средством (калькулятором статистических закономерностей). Исполнитель <b>не осуществляет инвестиционное консультирование</b>, не даёт индивидуальных инвестиционных рекомендаций (ФЗ № 39-ФЗ) и не осуществляет доверительное управление. Все сделки совершаются Пользователем самостоятельно на его личном брокерском счёте.</p>
671
+ <p><b>3. Параметры тарифов и предельные уровни доходности:</b>
672
+ <br>• <b>«Старт» (0 ₽)</b> — тестирование в Экспериментах и виртуальный счёт без реальной торговли;
673
+ <br>• <b>«Инвестор»</b> — до 2 брокерских счетов, лимит до 1 000 000 ₽ на каждом, торговля только от покупки («Только в рост (без шорта)»), предельный целевой темп доходности стратегии до 50% годовых;
674
+ <br>• <b>«Профи»</b> — до 5 брокерских счетов, лимит до 10 000 000 ₽ на каждом, торговля в рост и в падение (с шортами), предельный целевой темп доходности до 100% годовых;
675
+ <br>• <b>«VIP»</b> — по индивидуальному согласованию (VIP-токен), без ограничений по числу счетов, сумме капитала и потолку годовой доходности (включая полушаговое сопровождение H/2 и динамическую концентрацию слота).
676
+ </p>
677
+ <p><b>4. Запрет автоследования и ретрансляции сигналов:</b> Пользователю строго запрещается подключать к платформе счета, участвующие в сервисах автоследования или копирования сделок (как в роли ведущего/автора стратегии, так и в роли подписчика), а также передавать или перепродавать сигналы третьим лицам. При обнаружении признаков автоследования счёт автоматически блокируется.</p>
678
+ <p style="margin-bottom:0"><b>5. Декларация о рисках:</b> Торговля ценными бумагами сопряжена с риском потери части или всего инвестированного капитала. Результаты на исторических данных не гарантируют доходности в будущем. Полный текст: <a href="/legal" target="_blank" style="color:#93c5fd">открыть в новой вкладке →</a></p>
679
+ </div>
680
+ <div style="display:grid;gap:10px;font-size:13px;color:#e2e8f0;margin-bottom:16px">
681
+ <label style="display:flex;gap:10px;align-items:flex-start;cursor:pointer">
682
+ <input type="checkbox" id="offer-chk-1" style="width:17px;height:17px;margin-top:2px;flex:none">
683
+ <span>Я прочитал(а) и безоговорочно принимаю условия <a href="/legal#offer" target="_blank" style="color:#93c5fd">Договора публичной оферты</a> и <a href="/legal#privacy" target="_blank" style="color:#93c5fd">Политики обработки персональных данных</a>.</span>
684
+ </label>
685
+ <label style="display:flex;gap:10px;align-items:flex-start;cursor:pointer">
686
+ <input type="checkbox" id="offer-chk-2" style="width:17px;height:17px;margin-top:2px;flex:none">
687
+ <span>Я ознакомлен(а) с <a href="/legal#risks" target="_blank" style="color:#93c5fd">Декларацией о рисках</a>, понимаю, что сервис не является индивидуальной инвестиционной рекомендацией, и обязуюсь <b>не использовать автоследование</b> на подключаемых счетах.</span>
688
+ </label>
689
+ </div>
690
+ <div style="display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap">
691
+ <button class="btn" type="button" id="btn-offer-decline">Отказ (Отмена)</button>
692
+ <button class="btn primary" type="button" id="btn-offer-accept" disabled>Подписать оферту и продолжить →</button>
693
+ </div>
694
+ </div>
695
+ </div>
696
+
697
+ <!-- Модальное окно согласования VIP и ввода VIP-токена -->
698
+ <div class="modal" id="vip-modal" hidden>
699
+ <div class="modal-box" style="max-width:480px">
700
+ <h3>Тариф VIP · Индивидуальные условия</h3>
701
+ <p style="font-size:13.5px;color:var(--muted);margin:0 0 14px">Тариф VIP снимает все ограничения по числу счетов, объёму торгового капитала и потолку годовой доходности, активируя полный форсаж ядра (<code>H/2</code> + <code>Conviction Sizing</code>).</p>
702
+ <form id="vip-req-form" style="margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--line)">
703
+ <div style="font-size:13px;font-weight:600;color:#f59e0b">1. Оставить заявку на согласование условий VIP:</div>
704
+ <input name="name" placeholder="Ваше имя" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
705
+ <input name="contact" required placeholder="Ваш Telegram (@username) или телефон для связи" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
706
+ <input name="capitalRub" placeholder="Планируемый объём капитала (например, 25 млн ₽)" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
707
+ <button class="btn primary" type="submit" style="background:linear-gradient(135deg,#d97706,#f59e0b)">Отправить заявку администратору</button>
708
+ </form>
709
+ <form id="vip-token-form">
710
+ <div style="font-size:13px;font-weight:600;color:#93c5fd">2. Уже получили персональный VIP-токен от администратора?</div>
711
+ <div style="display:flex;gap:8px">
712
+ <input name="code" required placeholder="VIP-…" style="flex:1;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
713
+ <button class="btn primary" type="submit">Активировать VIP</button>
714
+ </div>
715
+ </form>
716
+ <div style="margin-top:12px;text-align:right">
717
+ <button class="btn" type="button" id="btn-vip-close" style="border:0;color:var(--muted)">Закрыть</button>
718
+ </div>
719
+ </div>
720
+ </div>
721
+
628
722
  <script>
629
723
  // ---------- 1. Анимированный граф перетоков капитала на фоне Hero ----------
630
724
  (() => {
@@ -736,72 +830,66 @@
736
830
  return;
737
831
  }
738
832
  cab.hidden = false;
739
- const key = currentUser.apiKey || '';
740
- const cmd = key ? `npx neuroalpha --key ${key}` : 'npx neuroalpha';
741
- const cliEl = document.getElementById('landing-cli-cmd');
742
- if (cliEl && key) cliEl.textContent = cmd;
743
833
  const navCta = document.getElementById('btn-nav-cta');
744
- if (navCta && currentUser.role === 'admin') {
745
- navCta.href = '/admin';
746
- navCta.textContent = 'Панель админа →';
834
+ if (navCta) {
835
+ if (currentUser.role === 'admin') {
836
+ navCta.href = '/admin';
837
+ navCta.textContent = 'Панель админа →';
838
+ } else {
839
+ navCta.href = '/';
840
+ navCta.textContent = 'Открыть терминал →';
841
+ }
747
842
  }
748
843
  const untilStr = currentUser.planUntil > Date.now()
749
844
  ? `оплачен до ${new Date(currentUser.planUntil).toLocaleDateString('ru-RU')}`
750
845
  : 'бессрочно';
846
+ const capLim = currentUser.limits?.maxCapitalPerAccountRub
847
+ ? (currentUser.limits.maxCapitalPerAccountRub >= 100000000 ? 'без ограничений' : `до ${(currentUser.limits.maxCapitalPerAccountRub / 1000000).toLocaleString('ru-RU')} млн ₽/счёт`)
848
+ : 'демо-режим';
849
+ const retLim = currentUser.limits?.maxAnnualReturnPct > 0
850
+ ? `до ${currentUser.limits.maxAnnualReturnPct}% годовых`
851
+ : 'без ограничений (VIP-форсаж)';
751
852
  cab.innerHTML = `<div class="cab-card">
752
853
  <div class="cab-grid">
753
854
  <div>
754
- <div class="sec-tag" style="margin-bottom:4px">Личный кабинет · Аккаунт активен</div>
855
+ <div class="sec-tag" style="margin-bottom:4px">Личный кабинет PaaS · Аккаунт активен</div>
755
856
  <h3 style="margin:0 0 8px;font-size:22px">${currentUser.name} <span style="color:var(--muted);font-weight:400;font-size:15px">(${currentUser.email})</span></h3>
756
857
  <div style="margin-bottom:14px;font-size:14.5px;color:#cbd5e1">
757
- Тариф: <b class="pos">${currentUser.planName}</b> (${untilStr}) · Доступно: до <b>${currentUser.limits?.maxBrokerAccounts ?? 0}</b> брокерских и <b>${currentUser.limits?.maxVirtualAccounts ?? 1}</b> виртуальных счетов, до <b>${currentUser.limits?.maxRunsPerDay ?? 2}</b> экспериментов/сутки и <b>${currentUser.limits?.maxChatPerDay ?? 10}</b> запросов к ИИ «Стратег» (Qwen3.8 27b).
858
+ Тариф: <b class="pos">${currentUser.planName}</b> (${untilStr}) · Брокерских счетов: до <b>${currentUser.limits?.maxBrokerAccounts ?? 0}</b> (${capLim}) · Предел доходности стратегии: <b>${retLim}</b> · Режим: <b>${currentUser.limits?.allowShorts ? 'в рост и в падение (с шортом)' : 'только в рост (без шорта)'}</b>.
758
859
  </div>
759
- <div style="margin-bottom:8px;font-size:13px;color:var(--muted)">Ваш персональный API-ключ (для запуска на вашем ПК):</div>
760
- <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
761
- <span class="key-pill" id="cab-apikey">${key}</span>
762
- <button type="button" class="btn" id="cab-copy-key" style="padding:7px 14px;font-size:13px">Скопировать APIKey</button>
763
- <button type="button" class="btn" id="cab-regen-key" style="padding:7px 14px;font-size:13px">Перевыпустить</button>
860
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
861
+ <input id="cab-promo-in" placeholder="Промокод или VIP-токен" style="padding:8px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text);font-size:13.5px;width:220px">
862
+ <button type="button" class="btn" id="cab-promo-btn" style="padding:8px 14px;font-size:13px">Активировать код</button>
764
863
  </div>
765
864
  </div>
766
865
  <div style="display:flex;flex-direction:column;gap:10px">
767
- <div style="font-size:13px;color:var(--muted)">Запустите платформу на своём ПК (в командной строке):</div>
768
- <div class="cli-box" style="padding:11px 14px;font-size:13px">
769
- <code>${cmd}</code>
770
- <button type="button" class="btn" id="cab-copy-cmd" style="padding:5px 10px;font-size:12px">Копировать</button>
771
- </div>
772
- <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:4px">
773
- <a class="btn primary" href="#pricing" style="flex:1">Сменить / продлить тариф</a>
774
- ${currentUser.role === 'admin' ? '<a class="btn" href="/admin">Консоль админа →</a><a class="btn" href="/?admin=1">Терминал →</a>' : ''}
866
+ <a class="btn primary lg" href="/" style="justify-content:center">Перейти в торговый терминал →</a>
867
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
868
+ <a class="btn" href="#pricing" style="flex:1">Сменить / продлить тариф</a>
869
+ ${currentUser.role === 'admin' ? '<a class="btn" href="/admin">Консоль админа →</a>' : ''}
775
870
  </div>
776
871
  </div>
777
872
  </div>
778
873
  </div>`;
779
874
 
780
- document.getElementById('cab-copy-key')?.addEventListener('click', (e) => {
781
- navigator.clipboard?.writeText(key);
782
- e.target.textContent = '✓ Скопировано';
783
- setTimeout(() => { e.target.textContent = 'Скопировать APIKey'; }, 2000);
784
- });
785
- document.getElementById('cab-copy-cmd')?.addEventListener('click', (e) => {
786
- navigator.clipboard?.writeText(cmd);
787
- e.target.textContent = '✓ Скопировано';
788
- setTimeout(() => { e.target.textContent = 'Копировать'; }, 2000);
789
- });
790
- document.getElementById('cab-regen-key')?.addEventListener('click', async () => {
791
- if (!confirm('Перевыпустить API-ключ? Прежний ключ перестанет работать.')) return;
792
- const r = await fetch('/api/auth/api-key/regenerate', { method: 'POST' });
793
- const j = await r.json();
794
- if (j.user) { currentUser = j.user; renderCabinet(); }
875
+ document.getElementById('cab-promo-btn')?.addEventListener('click', () => {
876
+ const code = document.getElementById('cab-promo-in')?.value.trim();
877
+ if (!code) return;
878
+ requireOfferAgreement(async () => {
879
+ const r = await fetch('/api/auth/redeem-promo', {
880
+ method: 'POST',
881
+ headers: { 'Content-Type': 'application/json' },
882
+ body: JSON.stringify({ code }),
883
+ });
884
+ const j = await r.json();
885
+ if (!r.ok) return alert(j.error || 'Ошибка активации кода');
886
+ currentUser = j.user;
887
+ renderCabinet();
888
+ alert(`Код успешно активирован! Ваш тариф: ${currentUser.planName}`);
889
+ });
795
890
  });
796
891
  }
797
892
 
798
- document.getElementById('btn-copy-landing-cli')?.addEventListener('click', (e) => {
799
- const txt = document.getElementById('landing-cli-cmd')?.textContent || 'npx neuroalpha';
800
- navigator.clipboard?.writeText(txt);
801
- e.target.textContent = '✓ Скопировано';
802
- setTimeout(() => { e.target.textContent = 'Скопировать'; }, 2000);
803
- });
804
-
805
893
  document.getElementById('oauth-btns')?.addEventListener('click', (e) => {
806
894
  const b = e.target.closest('button[data-oauth]');
807
895
  if (!b) return;
@@ -893,22 +981,61 @@
893
981
  const { runs } = await (await fetch('/api/public/showcase')).json();
894
982
  const box = document.getElementById('showcase-list');
895
983
  if (!runs?.length) return;
896
- box.innerHTML = runs.map((r, idx) => `<div class="sc-card">
984
+ const uniqueRuns = [];
985
+ const seenRet = new Set();
986
+ for (const x of runs) {
987
+ const rk = Math.round(Number(x.metrics?.returnPct || 0));
988
+ if (seenRet.has(rk)) continue;
989
+ seenRet.add(rk);
990
+ uniqueRuns.push(x);
991
+ if (uniqueRuns.length >= 3) break;
992
+ }
993
+ uniqueRuns.sort((a, b) => Number(a.metrics?.returnPct || 0) - Number(b.metrics?.returnPct || 0));
994
+ box.innerHTML = uniqueRuns.map((r, idx) => {
995
+ const from = r.period?.from || 0, till = r.period?.till || 0;
996
+ const years = Math.max(0.5, (till - from) / (365.25 * 86400000));
997
+ const ret = Number(r.metrics.returnPct || 0);
998
+ const cagr = years >= 0.25 && ret > -99 ? ((Math.pow(1 + ret / 100, 1 / years) - 1) * 100) : (ret / years);
999
+ const mktPct = Number(r.metrics.marketPct || -22.3);
1000
+ const isLong = r.params.modelDirection === 'long' || cagr <= 52;
1001
+ const isPro = !isLong && cagr <= 105;
1002
+ const tColor = r.tierColor || (isLong ? 'blue' : isPro ? 'indigo' : 'gold');
1003
+ const tBadge = r.tierBadge || (isLong ? 'Для тарифа «Инвестор»' : isPro ? 'Для тарифа «Профи»' : '👑 Для тарифа «VIP»');
1004
+ const tNote = r.tierNote || (isLong
1005
+ ? 'Торговля только от покупки (без шортов) · потолок тарифа до 50% годовых · до 1 млн ₽/счёт'
1006
+ : isPro
1007
+ ? 'Заработок на росте и падении акций (с шортами) · потолок тарифа до 100% годовых · до 10 млн ₽/счёт'
1008
+ : 'Полная мощность без потолка доходности · форсаж H/2 + концентрация слота Conviction Sizing');
1009
+ const badgeStyle = tColor === 'gold'
1010
+ ? 'background:rgba(245,158,11,0.18);border:1px solid rgba(245,158,11,0.5);color:#fbbf24'
1011
+ : tColor === 'indigo'
1012
+ ? 'background:rgba(99,102,241,0.2);border:1px solid rgba(129,140,248,0.5);color:#a5b4fc'
1013
+ : 'background:rgba(59,130,246,0.16);border:1px solid rgba(96,165,250,0.45);color:#93c5fd';
1014
+ const cardBorder = tColor === 'gold' ? 'border-color:rgba(245,158,11,0.4)' : '';
1015
+ return `<div class="sc-card" style="${cardBorder}">
1016
+ <div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:8px">
1017
+ <span style="display:inline-block;padding:3px 10px;border-radius:999px;font-size:11.5px;font-weight:700;letter-spacing:0.3px;${badgeStyle}">${tBadge}</span>
1018
+ <span style="font-size:12px;color:var(--muted)">Бумаг в портфеле: ${r.params.modelTopK || 2}</span>
1019
+ </div>
897
1020
  <div class="sc-head">
898
1021
  <div>
899
- <div style="font-weight:700;font-size:18px">${r.title}</div>
900
- <div style="font-size:13px;color:var(--muted)">Горизонт прогноза: ${r.params.modelHorizon || 2} ч · Бумаг в портфеле: ${r.params.modelTopK || 2}</div>
1022
+ <div style="font-weight:700;font-size:17px">${r.title}</div>
1023
+ <div style="font-size:12.5px;color:var(--muted);margin-top:3px">${tNote}</div>
1024
+ </div>
1025
+ <div style="text-align:right;flex:none">
1026
+ <div class="sc-ret pos">${fmt(cagr, 1)}% <span style="font-size:13px;font-weight:600">/ год</span></div>
1027
+ <div style="font-size:12px;color:var(--muted)">всего ${fmt(ret, 1)}% за ${years.toFixed(1).replace('.', ',')} г.</div>
901
1028
  </div>
902
- <div class="sc-ret pos">${fmt(r.metrics.returnPct, 1)}%</div>
903
1029
  </div>
904
1030
  ${sparkSvg(r.curve, r.marketCurve, idx)}
905
1031
  <div class="sc-kpis">
906
- <div><span class="muted">Индекс Мосбиржи</span><b class="neg">${fmt(r.metrics.marketPct, 1)}%</b></div>
907
- <div><span class="muted">Сделок за период</span><b>${r.metrics.trades || 0}</b></div>
1032
+ <div><span class="muted">Индекс Мосбиржи</span><b class="${mktPct >= 0 ? 'pos' : 'neg'}">${fmt(mktPct, 1)}%</b></div>
1033
+ <div><span class="muted">Макс. просадка</span><b>${fmt(r.metrics.maxDrawdownPct, 1).replace('+','')}%</b></div>
908
1034
  <div><span class="muted">На 1 ₽ убытка</span><b>${fmt(r.metrics.profitFactor, 2).replace('+','')} ₽</b></div>
909
1035
  </div>
910
- <a class="btn" href="#cli-sec">Запустить эту стратегию через npx →</a>
911
- </div>`).join('');
1036
+ <a class="btn" href="#pricing">Выбрать этот профиль в облаке →</a>
1037
+ </div>`;
1038
+ }).join('');
912
1039
  } catch {}
913
1040
  }
914
1041
 
@@ -945,20 +1072,106 @@
945
1072
  document.getElementById('auth-close').addEventListener('click', () => { document.getElementById('auth-modal').hidden = true; });
946
1073
  document.getElementById('auth-toggle').addEventListener('click', () => openAuth(!isReg));
947
1074
 
948
- async function startCheckout(plan) {
1075
+ let onOfferAccepted = null;
1076
+ const offerModal = document.getElementById('offer-modal');
1077
+ const offerChk1 = document.getElementById('offer-chk-1');
1078
+ const offerChk2 = document.getElementById('offer-chk-2');
1079
+ const offerAcceptBtn = document.getElementById('btn-offer-accept');
1080
+
1081
+ function syncOfferBtn() {
1082
+ if (offerAcceptBtn) offerAcceptBtn.disabled = !(offerChk1?.checked && offerChk2?.checked);
1083
+ }
1084
+ offerChk1?.addEventListener('change', syncOfferBtn);
1085
+ offerChk2?.addEventListener('change', syncOfferBtn);
1086
+
1087
+ function requireOfferAgreement(actionFn) {
1088
+ onOfferAccepted = actionFn;
1089
+ if (offerChk1) offerChk1.checked = false;
1090
+ if (offerChk2) offerChk2.checked = false;
1091
+ syncOfferBtn();
1092
+ offerModal.hidden = false;
1093
+ }
1094
+
1095
+ document.getElementById('btn-offer-decline')?.addEventListener('click', () => {
1096
+ offerModal.hidden = true;
1097
+ onOfferAccepted = null;
1098
+ });
1099
+ offerAcceptBtn?.addEventListener('click', async () => {
1100
+ offerAcceptBtn.disabled = true;
949
1101
  try {
950
- const res = await fetch('/api/billing/checkout', {
1102
+ await fetch('/api/auth/accept-offer', { method: 'POST' }).catch(() => {});
1103
+ offerModal.hidden = true;
1104
+ if (onOfferAccepted) await onOfferAccepted();
1105
+ } finally {
1106
+ onOfferAccepted = null;
1107
+ syncOfferBtn();
1108
+ }
1109
+ });
1110
+
1111
+ async function startCheckout(plan) {
1112
+ requireOfferAgreement(async () => {
1113
+ try {
1114
+ const res = await fetch('/api/billing/checkout', {
1115
+ method: 'POST',
1116
+ headers: { 'Content-Type': 'application/json' },
1117
+ body: JSON.stringify({ plan, months: selectedMonths, returnOrigin: location.origin }),
1118
+ });
1119
+ const j = await res.json();
1120
+ if (!res.ok) throw new Error(j.error || 'Ошибка перехода к оплате');
1121
+ if (j.confirmationUrl) location.href = j.confirmationUrl;
1122
+ } catch (e) {
1123
+ alert(e.message);
1124
+ }
1125
+ });
1126
+ }
1127
+
1128
+ document.getElementById('btn-vip-req')?.addEventListener('click', () => {
1129
+ document.getElementById('vip-modal').hidden = false;
1130
+ });
1131
+ document.getElementById('btn-vip-token-open')?.addEventListener('click', () => {
1132
+ if (!currentUser) { openAuth(true); return; }
1133
+ document.getElementById('vip-modal').hidden = false;
1134
+ document.querySelector('#vip-token-form input[name=code]')?.focus();
1135
+ });
1136
+ document.getElementById('btn-vip-close')?.addEventListener('click', () => {
1137
+ document.getElementById('vip-modal').hidden = true;
1138
+ });
1139
+ document.getElementById('vip-req-form')?.addEventListener('submit', async (e) => {
1140
+ e.preventDefault();
1141
+ const f = e.target.elements;
1142
+ const res = await fetch('/api/auth/vip-request', {
1143
+ method: 'POST',
1144
+ headers: { 'Content-Type': 'application/json' },
1145
+ body: JSON.stringify({ name: f.name.value, contact: f.contact.value, capitalRub: f.capitalRub.value }),
1146
+ });
1147
+ const j = await res.json();
1148
+ if (!res.ok) return alert(j.error || 'Ошибка отправки заявки');
1149
+ document.getElementById('vip-modal').hidden = true;
1150
+ alert('Заявка на тариф VIP отправлена администратору! Мы свяжемся с вами для согласования условий и выдачи персонального VIP-токена.');
1151
+ });
1152
+ document.getElementById('vip-token-form')?.addEventListener('submit', (e) => {
1153
+ e.preventDefault();
1154
+ const code = e.target.elements.code.value.trim();
1155
+ if (!code) return;
1156
+ if (!currentUser) {
1157
+ document.getElementById('vip-modal').hidden = true;
1158
+ openAuth(true);
1159
+ return;
1160
+ }
1161
+ document.getElementById('vip-modal').hidden = true;
1162
+ requireOfferAgreement(async () => {
1163
+ const r = await fetch('/api/auth/redeem-promo', {
951
1164
  method: 'POST',
952
1165
  headers: { 'Content-Type': 'application/json' },
953
- body: JSON.stringify({ plan, months: selectedMonths, returnOrigin: location.origin }),
1166
+ body: JSON.stringify({ code }),
954
1167
  });
955
- const j = await res.json();
956
- if (!res.ok) throw new Error(j.error || 'Ошибка перехода к оплате');
957
- if (j.confirmationUrl) location.href = j.confirmationUrl;
958
- } catch (e) {
959
- alert(e.message);
960
- }
961
- }
1168
+ const j = await r.json();
1169
+ if (!r.ok) return alert(j.error || 'Не удалось активировать VIP-токен');
1170
+ currentUser = j.user;
1171
+ renderCabinet();
1172
+ alert(`Персональный VIP-токен активирован! Ваш тариф: ${currentUser.planName}`);
1173
+ });
1174
+ });
962
1175
 
963
1176
  document.querySelectorAll('[data-buy]').forEach((b) => b.addEventListener('click', () => {
964
1177
  const plan = b.dataset.buy;