neuroalpha 0.3.15 → 0.3.17

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/public/legal.html CHANGED
@@ -49,10 +49,9 @@
49
49
 
50
50
  <h3>1.2. Тарифные планы и технические параметры мощности</h3>
51
51
  <ul>
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>
52
+ <li><b>Тариф «Инвестор»:</b> позволяет подключить 1 брокерский счёт со стартовым лимитом капитала до 1 000 000 ₽ (+ реинвестирование всей заработанной прибыли), работающий исключительно в режиме покупок («Только в рост (без шорта)») с предельным целевым темпом доходности стратегии до 60% годовых.</li>
53
+ <li><b>Тариф «Профи»:</b> позволяет подключить до 5 брокерских счетов со стартовым лимитом капитала до 1 000 000 ₽ на каждом счёте (+ реинвестирование всей заработанной прибыли), включает режим работы на росте и падении акций («В рост и в падение (с шортом)») с предельным целевым темпом доходности стратегии до 120% годовых.</li>
54
+ <li><b>Тариф «VIP»:</b> предоставляется по индивидуальному согласованию с Администрацией платформы (через персональный VIP-токен), количество счетов и начальная сумма торговли определяются по согласованию, включает работу с шортами и предельный целевой темп доходности стратегии до 240% годовых.</li>
56
55
  </ul>
57
56
 
58
57
  <h3>1.3. Запрет использования автоследования и ретрансляции сигналов</h3>
package/public/styles.css CHANGED
@@ -187,8 +187,12 @@ body.light .tbadge.run { background: #dbeafe; border-color: #93c5fd; }
187
187
  .toolbar { display: flex; gap: 8px; align-items: center; }
188
188
  .toolbar select { flex: 1; min-width: 0; }
189
189
  .progress { position: relative; height: 22px; background: var(--panel); border-radius: 6px; overflow: hidden; margin-top: 10px; }
190
- .progress .bar { position: absolute; inset: 0 auto 0 0; width: 0; background: #24467f; transition: width .2s; }
190
+ .progress .bar { position: absolute; inset: 0 auto 0 0; width: 0; background: linear-gradient(90deg, #24467f 0%, #3b82f6 50%, #24467f 100%); background-size: 200% 100%; animation: progShimmer 1.6s linear infinite; transition: width .25s; }
191
+ @keyframes progShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
191
192
  .progress span { position: relative; font-size: 12px; padding: 0 8px; line-height: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
193
+ .ld-calc-banner { display: flex; align-items: center; gap: 12px; background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.45); border-radius: 10px; padding: 12px 16px; margin-bottom: 12px; animation: calcPulse 1.8s ease-in-out infinite; }
194
+ @keyframes calcPulse { 0%, 100% { border-color: rgba(59, 130, 246, 0.35); background: rgba(59, 130, 246, 0.09); } 50% { border-color: rgba(59, 130, 246, 0.75); background: rgba(59, 130, 246, 0.17); } }
195
+ .ld-calc-spin { width: 20px; height: 20px; border: 2.5px solid var(--accent); border-right-color: transparent; border-radius: 50%; flex-shrink: 0; animation: tspin .75s linear infinite; }
192
196
  .empty { margin-top: 30px; color: var(--muted); line-height: 1.7; max-width: 640px; }
193
197
  .empty.live-dash { margin-top: 14px; max-width: none; color: var(--text); line-height: 1.45; }
194
198
  .empty b { color: var(--text); }
@@ -277,7 +281,7 @@ body.light .think-body { color: #4b5563; }
277
281
  body.light .scard { background: var(--panel); }
278
282
  body.light .snav button.active { background: #eaeef2; color: var(--text); }
279
283
  body.light .snav button:hover { background: #f0f3f6; }
280
- body.light .progress .bar { background: #b6d0ff; }
284
+ body.light .progress .bar { background: linear-gradient(90deg, #b6d0ff 0%, #60a5fa 50%, #b6d0ff 100%); background-size: 200% 100%; }
281
285
  body.light .feed-down, body.light #toast div { box-shadow: 0 6px 20px rgba(15, 23, 42, 0.14); }
282
286
  body.light ::-webkit-scrollbar-thumb { background: #c1c8d1; border-color: var(--bg); }
283
287
  body.light ::-webkit-scrollbar-thumb:hover { background: #9aa4b2; }
@@ -15,7 +15,7 @@ import { startModelResearch, cancelModelResearch, startStabilityCheck, cancelSta
15
15
  import { forecastNow } from '../model/forecast.js';
16
16
  import { profilesInfo, startProfilesRefresh } from '../model/profiles.js';
17
17
  import { live } from '../model/live.js';
18
- import { brokersState, saveBrokerAccount, deleteBrokerAccount, syncBrokerAccount, discoverBrokerAccounts, freezeBrokerBaseline, fetchBrokerHistory, prepareBrokerOrders, executeBrokerOrders, rejectBrokerOrders } from '../brokers/index.js';
18
+ import { brokersState, saveBrokerAccount, deleteBrokerAccount, syncBrokerAccount, discoverBrokerAccounts, freezeBrokerBaseline, fetchBrokerHistory, prepareBrokerOrders, executeBrokerOrders, rejectBrokerOrders, cancelBrokerOrder } from '../brokers/index.js';
19
19
  import { listNews } from '../news/index.js';
20
20
  import * as strategist from '../strategist/index.js';
21
21
  import { hub } from '../hub.js';
@@ -436,8 +436,11 @@ export function createRouter() {
436
436
 
437
437
  r.post('/model/research', wrap((req, res) => {
438
438
  const plan = effectivePlan(req.user);
439
+ if (!plan.allowResearch && req.user?.role !== 'admin') {
440
+ throw new Error('Раздел исследований доступен только в режиме «Хозяин».');
441
+ }
439
442
  if (countUserRunsToday() >= plan.maxRunsPerDay) {
440
- throw new Error(`Достигнут дневной лимит исследований по тарифу «${plan.name}» (${plan.maxRunsPerDay} в сутки). Перейдите на тариф «Инвестор» или «Профи».`);
443
+ throw new Error(`Достигнут дневной лимит исследований (${plan.maxRunsPerDay} в сутки).`);
441
444
  }
442
445
  const body = req.body || {};
443
446
  const merged = { ...getSettings(), ...body };
@@ -469,7 +472,10 @@ export function createRouter() {
469
472
  const plan = effectivePlan(req.user);
470
473
  const cur = brokersState().accounts.length;
471
474
  if (!req.body?.id && cur >= plan.maxBrokerAccounts) {
472
- throw new Error(`По вашему тарифу «${plan.name}» доступно подключение до ${plan.maxBrokerAccounts} брокерских счетов. Перейдите на тариф «Инвестор» или «Профи».`);
475
+ if ((plan.maxBrokerAccounts ?? 0) < 1) {
476
+ throw new Error('Для подключения брокерского счёта оформите подписку «Инвестор» (3 900 ₽/мес), «Профи» (9 900 ₽/мес) или «VIP».');
477
+ }
478
+ throw new Error(`По вашему тарифу «${plan.name}» доступно подключение до ${plan.maxBrokerAccounts} брокерских счетов. Перейдите на тариф «Профи» или «VIP».`);
473
479
  }
474
480
  res.json(saveBrokerAccount(req.body || {}));
475
481
  }));
@@ -481,6 +487,7 @@ export function createRouter() {
481
487
  r.post('/brokers/:id/prepare', wrap(async (req, res) => res.json(await prepareBrokerOrders(req.params.id))));
482
488
  r.post('/brokers/:id/execute', wrap(async (req, res) => res.json(await executeBrokerOrders(req.params.id))));
483
489
  r.post('/brokers/:id/reject', wrap((req, res) => res.json(rejectBrokerOrders(req.params.id))));
490
+ r.post('/brokers/:id/cancel-order', wrap(async (req, res) => res.json(await cancelBrokerOrder(req.params.id, req.body?.orderId || null))));
484
491
 
485
492
  // ---------- новости (только для прогноза «сейчас» и живой проверки) ----------
486
493
  r.get('/news', (req, res) => res.json({
@@ -74,61 +74,80 @@ export function verifyPassword(password, stored) {
74
74
  return hash.length === target.length && crypto.timingSafeEqual(hash, target);
75
75
  }
76
76
 
77
- // ---------- пользователи, тарифные планы (4 уровня) и сессии ----------
77
+ // ---------- пользователи, тарифные планы (Инвестор, Профи, VIP + режим Хозяин) и сессии ----------
78
78
  export const PLANS = {
79
- free: {
80
- id: 'free',
81
- name: 'Старт (Тест-драйв)',
79
+ none: {
80
+ id: 'none',
81
+ name: 'Без подписки',
82
82
  priceRub: 0,
83
- maxRunsPerDay: 20,
84
- maxVirtualAccounts: 1,
83
+ maxRunsPerDay: 0,
84
+ maxVirtualAccounts: 0,
85
85
  maxBrokerAccounts: 0,
86
86
  maxCapitalPerAccountRub: 0,
87
87
  allowShorts: false,
88
88
  allowVipBoost: false,
89
- maxAnnualReturnPct: 50,
90
- maxChatPerDay: 20,
89
+ allowResearch: false,
90
+ maxAnnualReturnPct: 0,
91
+ maxChatPerDay: 10,
91
92
  },
92
93
  investor: {
93
94
  id: 'investor',
94
95
  name: 'Инвестор',
95
96
  priceRub: 3900,
96
- maxRunsPerDay: 50,
97
- maxVirtualAccounts: 3,
98
- maxBrokerAccounts: 2,
97
+ maxRunsPerDay: 0,
98
+ maxVirtualAccounts: 0,
99
+ maxBrokerAccounts: 1,
99
100
  maxCapitalPerAccountRub: 1000000,
100
101
  allowShorts: false,
101
102
  allowVipBoost: false,
102
- maxAnnualReturnPct: 50,
103
+ allowResearch: false,
104
+ maxAnnualReturnPct: 60,
103
105
  maxChatPerDay: 200,
104
106
  },
105
107
  pro: {
106
108
  id: 'pro',
107
109
  name: 'Профи',
108
110
  priceRub: 9900,
109
- maxRunsPerDay: 500,
110
- maxVirtualAccounts: 5,
111
+ maxRunsPerDay: 0,
112
+ maxVirtualAccounts: 0,
111
113
  maxBrokerAccounts: 5,
112
- maxCapitalPerAccountRub: 10000000,
114
+ maxCapitalPerAccountRub: 1000000,
113
115
  allowShorts: true,
114
116
  allowVipBoost: false,
115
- maxAnnualReturnPct: 100,
117
+ allowResearch: false,
118
+ maxAnnualReturnPct: 120,
116
119
  maxChatPerDay: 1000,
117
120
  },
118
121
  vip: {
119
122
  id: 'vip',
120
- name: 'VIP (Без ограничений)',
123
+ name: 'VIP',
121
124
  priceRub: 0, // по индивидуальному согласованию / VIP-токену от администратора
122
125
  customPrice: true,
123
- maxRunsPerDay: 10000,
124
- maxVirtualAccounts: 20,
126
+ maxRunsPerDay: 0,
127
+ maxVirtualAccounts: 0,
125
128
  maxBrokerAccounts: 50,
126
129
  maxCapitalPerAccountRub: 1000000000,
127
130
  allowShorts: true,
128
131
  allowVipBoost: true,
129
- maxAnnualReturnPct: 0, // 0 = без потолка доходности (полный форсаж H/2 + Conviction Sizing)
132
+ allowResearch: false,
133
+ maxAnnualReturnPct: 240,
130
134
  maxChatPerDay: 5000,
131
135
  },
136
+ owner: {
137
+ id: 'owner',
138
+ name: 'Хозяин (Без ограничений)',
139
+ priceRub: 0,
140
+ isOwner: true,
141
+ maxRunsPerDay: 10000,
142
+ maxVirtualAccounts: 20,
143
+ maxBrokerAccounts: 100,
144
+ maxCapitalPerAccountRub: 1000000000,
145
+ allowShorts: true,
146
+ allowVipBoost: true,
147
+ allowResearch: true,
148
+ maxAnnualReturnPct: 0, // 0 = без потолка доходности (полный форсаж >400% годовых)
149
+ maxChatPerDay: 10000,
150
+ },
132
151
  };
133
152
 
134
153
  export function generateApiKey() {
@@ -207,25 +226,23 @@ async function patchUserLicense(cleanKey, baseUrl) {
207
226
  }
208
227
 
209
228
  export function effectivePlan(user) {
210
- if (!user) return PLANS.free;
211
- // В локальном режиме: если активирован лицензионный ключ — действует тариф подписки;
212
- // если это явно админ — действует VIP-тариф без ограничений
229
+ if (!user) return PLANS.none;
213
230
  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;
231
+ if (user.plan && PLANS[user.plan] && user.plan !== 'owner') {
232
+ if (user.plan !== 'none' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.none;
216
233
  return PLANS[user.plan];
217
234
  }
218
235
  const lic = getLocalLicense();
219
236
  if (lic?.plan && PLANS[lic.plan]) {
220
- if (lic.plan !== 'free' && lic.planUntil > 0 && lic.planUntil < Date.now()) return PLANS.free;
237
+ if (lic.plan !== 'none' && lic.planUntil > 0 && lic.planUntil < Date.now()) return PLANS.none;
221
238
  return PLANS[lic.plan];
222
239
  }
223
- if (process.env.LOCAL_STRICT_LICENSE === '1') return PLANS.free;
224
- return PLANS.vip;
240
+ if (process.env.LOCAL_STRICT_LICENSE === '1') return PLANS.none;
241
+ return PLANS.owner;
225
242
  }
226
- if (user.role === 'admin') return PLANS[user.plan] || PLANS.vip;
227
- const p = PLANS[user.plan] || PLANS.free;
228
- if (p.id !== 'free' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.free;
243
+ if (user.role === 'admin' || user.plan === 'owner') return PLANS[user.plan] || PLANS.owner;
244
+ const p = PLANS[user.plan] || PLANS.none;
245
+ if (p.id !== 'none' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.none;
229
246
  return p;
230
247
  }
231
248
 
@@ -235,11 +252,13 @@ export function publicUser(u) {
235
252
  try { u.api_key = ensureApiKey(u.id); } catch { /* ignore */ }
236
253
  }
237
254
  const plan = effectivePlan(u);
255
+ const isOwner = Boolean(u.role === 'admin' || plan.id === 'owner' || plan.allowResearch);
238
256
  return {
239
257
  id: u.id,
240
258
  email: u.email,
241
259
  name: u.name || 'Инвестор',
242
260
  role: u.role || 'user',
261
+ isOwner,
243
262
  plan: plan.id,
244
263
  planName: plan.name,
245
264
  planUntil: u.plan_until || 0,
@@ -336,7 +355,7 @@ export function registerUser({ email, password, name }) {
336
355
  const code = `NA-${crypto.randomBytes(3).toString('hex').toUpperCase()}`;
337
356
  const apiKey = generateApiKey();
338
357
  const r = db.prepare(`INSERT INTO users (email, name, pass_hash, role, plan, plan_until, notify_code, api_key, created_at)
339
- VALUES (?, ?, ?, 'user', 'free', 0, ?, ?, ?)`).run(cleanEmail, String(name || cleanEmail.split('@')[0]).trim(), hashPassword(password), code, apiKey, Date.now());
358
+ VALUES (?, ?, ?, 'user', 'none', 0, ?, ?, ?)`).run(cleanEmail, String(name || cleanEmail.split('@')[0]).trim(), hashPassword(password), code, apiKey, Date.now());
340
359
  return getUserById(Number(r.lastInsertRowid));
341
360
  }
342
361
 
@@ -362,7 +381,7 @@ export function listAllUsers() {
362
381
 
363
382
  export function setUserPlan(userId, plan, days = 30) {
364
383
  if (!(plan in PLANS)) throw new Error(`Неизвестный тариф: ${plan}`);
365
- const until = plan === 'free' ? 0 : Date.now() + Math.max(1, Number(days) || 30) * 86400000;
384
+ const until = (plan === 'none' || plan === 'owner') ? 0 : Date.now() + Math.max(1, Number(days) || 30) * 86400000;
366
385
  db.prepare('UPDATE users SET plan = ?, plan_until = ? WHERE id = ?').run(plan, until, Number(userId));
367
386
  return publicUser(getUserById(userId));
368
387
  }
@@ -374,7 +393,7 @@ export function createPromoCode({ code, plan = 'investor', days = 14, maxUses =
374
393
  clean = `VIP-${crypto.randomBytes(3).toString('hex').toUpperCase()}-${crypto.randomBytes(2).toString('hex').toUpperCase()}`;
375
394
  }
376
395
  if (clean.length < 3) throw new Error('Код должен содержать минимум 3 символа (латиница/цифры)');
377
- if (!(plan in PLANS) || plan === 'free') throw new Error('Выберите тариф Инвестор, Профи или VIP');
396
+ if (!(plan in PLANS) || plan === 'none') throw new Error('Выберите тариф Инвестор, Профи или VIP');
378
397
  const d = Math.max(1, Math.min(3650, Math.round(Number(days) || (plan === 'vip' ? 365 : 14))));
379
398
  const m = Math.max(1, Math.min(10000, Math.round(Number(maxUses) || (plan === 'vip' ? 1 : 50))));
380
399
  db.prepare(`INSERT OR REPLACE INTO promocodes (code, plan, days, max_uses, used_count, created_at)