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/README.md +79 -170
- package/config/strategy.json +2 -2
- package/docs//320/221/320/240/320/236/320/232/320/225/320/240/320/253_API.md +4 -3
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +10 -17
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +96 -156
- 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 +29 -45
- package/package.json +1 -1
- package/public/admin.html +9 -8
- package/public/app.js +464 -930
- package/public/index.html +148 -223
- package/public/landing.html +19 -33
- package/public/legal.html +3 -4
- package/public/styles.css +6 -2
- package/server/api/routes.js +10 -3
- package/server/auth/index.js +53 -34
- package/server/brokers/index.js +434 -104
- package/server/data/moex.js +74 -23
- package/server/db.js +3 -2
- package/server/model/dataset.js +10 -2
- package/server/model/forecast.js +56 -19
- package/server/model/live.js +30 -6
- package/server/model/models.js +113 -31
- package/server/model/research.js +100 -26
- package/server/params-meta.js +19 -19
- package/server/strategist/index.js +423 -484
- package/server/strategist/tools.js +486 -303
package/public/legal.html
CHANGED
|
@@ -49,10 +49,9 @@
|
|
|
49
49
|
|
|
50
50
|
<h3>1.2. Тарифные планы и технические параметры мощности</h3>
|
|
51
51
|
<ul>
|
|
52
|
-
<li><b>Тариф
|
|
53
|
-
<li><b>Тариф
|
|
54
|
-
<li><b>Тариф
|
|
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 .
|
|
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; }
|
package/server/api/routes.js
CHANGED
|
@@ -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(`Достигнут дневной лимит исследований
|
|
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
|
-
|
|
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({
|
package/server/auth/index.js
CHANGED
|
@@ -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
|
-
// ---------- пользователи, тарифные планы (
|
|
77
|
+
// ---------- пользователи, тарифные планы (Инвестор, Профи, VIP + режим Хозяин) и сессии ----------
|
|
78
78
|
export const PLANS = {
|
|
79
|
-
|
|
80
|
-
id: '
|
|
81
|
-
name: '
|
|
79
|
+
none: {
|
|
80
|
+
id: 'none',
|
|
81
|
+
name: 'Без подписки',
|
|
82
82
|
priceRub: 0,
|
|
83
|
-
maxRunsPerDay:
|
|
84
|
-
maxVirtualAccounts:
|
|
83
|
+
maxRunsPerDay: 0,
|
|
84
|
+
maxVirtualAccounts: 0,
|
|
85
85
|
maxBrokerAccounts: 0,
|
|
86
86
|
maxCapitalPerAccountRub: 0,
|
|
87
87
|
allowShorts: false,
|
|
88
88
|
allowVipBoost: false,
|
|
89
|
-
|
|
90
|
-
|
|
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:
|
|
97
|
-
maxVirtualAccounts:
|
|
98
|
-
maxBrokerAccounts:
|
|
97
|
+
maxRunsPerDay: 0,
|
|
98
|
+
maxVirtualAccounts: 0,
|
|
99
|
+
maxBrokerAccounts: 1,
|
|
99
100
|
maxCapitalPerAccountRub: 1000000,
|
|
100
101
|
allowShorts: false,
|
|
101
102
|
allowVipBoost: false,
|
|
102
|
-
|
|
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:
|
|
110
|
-
maxVirtualAccounts:
|
|
111
|
+
maxRunsPerDay: 0,
|
|
112
|
+
maxVirtualAccounts: 0,
|
|
111
113
|
maxBrokerAccounts: 5,
|
|
112
|
-
maxCapitalPerAccountRub:
|
|
114
|
+
maxCapitalPerAccountRub: 1000000,
|
|
113
115
|
allowShorts: true,
|
|
114
116
|
allowVipBoost: false,
|
|
115
|
-
|
|
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:
|
|
124
|
-
maxVirtualAccounts:
|
|
126
|
+
maxRunsPerDay: 0,
|
|
127
|
+
maxVirtualAccounts: 0,
|
|
125
128
|
maxBrokerAccounts: 50,
|
|
126
129
|
maxCapitalPerAccountRub: 1000000000,
|
|
127
130
|
allowShorts: true,
|
|
128
131
|
allowVipBoost: true,
|
|
129
|
-
|
|
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.
|
|
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 !== '
|
|
215
|
-
if (user.plan !== '
|
|
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 !== '
|
|
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.
|
|
224
|
-
return PLANS.
|
|
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.
|
|
227
|
-
const p = PLANS[user.plan] || PLANS.
|
|
228
|
-
if (p.id !== '
|
|
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', '
|
|
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 === '
|
|
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 === '
|
|
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)
|