neuroalpha 0.3.14 → 0.3.16
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 +80 -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 +119 -0
- 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 +97 -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 +591 -832
- package/public/index.html +142 -186
- package/public/landing.html +19 -33
- package/public/legal.html +3 -4
- package/public/styles.css +10 -4
- package/server/api/routes.js +10 -3
- package/server/auth/index.js +53 -34
- package/server/brokers/index.js +718 -107
- 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 +16 -16
- package/server/strategist/index.js +4 -0
- package/server/strategist/tools.js +229 -0
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)
|