neuroalpha 0.2.0 → 0.3.12
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 +21 -0
- package/README.md +7 -3
- package/bin/cli.js +69 -69
- package/config/seed-run.json +1 -0
- package/config/strategy.json +4 -10
- package/deploy/install.sh +128 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +103 -0
- 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 +171 -0
- package/package.json +15 -5
- package/public/admin.html +426 -0
- package/public/app.js +741 -251
- package/public/index.html +45 -20
- package/public/landing.html +1004 -870
- package/public/legal.html +22 -0
- package/public/styles.css +49 -15
- package/server/api/routes.js +176 -23
- package/server/auth/index.js +544 -340
- package/server/billing/yookassa.js +1 -0
- package/server/brokers/index.js +1224 -645
- package/server/config.js +6 -4
- package/server/data/moex.js +1 -0
- package/server/db.js +53 -2
- package/server/hub.js +1 -0
- package/server/index.js +10 -2
- package/server/llm/client.js +1 -0
- package/server/llm/providers.js +50 -3
- package/server/model/dataset.js +362 -352
- package/server/model/forecast.js +15 -21
- package/server/model/linalg.js +1 -0
- package/server/model/live.js +489 -486
- package/server/model/metrics.js +1 -0
- package/server/model/models.js +31 -21
- package/server/model/profiles.js +1 -0
- package/server/model/research.js +119 -172
- package/server/model/vocab.js +1 -0
- package/server/news/index.js +1 -0
- package/server/notify/index.js +304 -168
- package/server/params-meta.js +20 -25
- package/server/settings.js +1 -0
- package/server/strategist/analytics.js +1 -0
- package/server/strategist/index.js +6 -6
- package/server/strategist/tools.js +2 -1
- package/server/util.js +1 -0
- package/server/model/gpu-worker.html +0 -11
- package/server/model/gpu-worker.js +0 -60
- package/server/model/tf-node.js +0 -26
- package/server/model/transformer-core.js +0 -330
- package/server/model/transformer-gpu.js +0 -221
- package/server/model/transformer-pool.js +0 -200
- package/server/model/transformer-worker.js +0 -28
- package/server/model/transformer.js +0 -57
package/server/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -31,14 +32,15 @@ function resolveApiKey() {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export const llmConfig = {
|
|
34
|
-
provider: (process.env.LLM_PROVIDER || '
|
|
35
|
-
baseUrl: process.env.LLM_BASE_URL || '',
|
|
36
|
-
model: process.env.LLM_MODEL || '',
|
|
35
|
+
provider: (process.env.LLM_PROVIDER || 'openai').toLowerCase(),
|
|
36
|
+
baseUrl: process.env.LLM_BASE_URL || 'https://models.odant.org/v1/chat/completions',
|
|
37
|
+
model: process.env.LLM_MODEL || 'Qwen/Qwen3.8-27B-FP8',
|
|
37
38
|
apiKey: resolveApiKey(),
|
|
38
|
-
reasoningEffort: process.env.LLM_REASONING_EFFORT || '',
|
|
39
|
+
reasoningEffort: process.env.LLM_REASONING_EFFORT || 'low',
|
|
39
40
|
maxTokens: Number(process.env.LLM_MAX_TOKENS || 4000),
|
|
40
41
|
timeoutMs: Number(process.env.LLM_TIMEOUT_MS || 90000),
|
|
41
42
|
};
|
|
42
43
|
llmConfig.enabled = llmConfig.provider !== 'none' && !!llmConfig.baseUrl && !!llmConfig.model && !!llmConfig.apiKey;
|
|
43
44
|
|
|
44
45
|
export const PORT = Number(process.env.PORT || 3000);
|
|
46
|
+
export const DEFAULT_CLOUD_URL = (process.env.CLOUD_URL || 'https://neuroalpha.odant.org').replace(/\/+$/, '');
|
package/server/data/moex.js
CHANGED
package/server/db.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -194,10 +195,12 @@ for (const [tbl, col, def] of [
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
// Гарантируем наличие
|
|
198
|
+
// Гарантируем наличие локального пользователя по умолчанию (id = 1)
|
|
198
199
|
if (!db.prepare('SELECT id FROM users WHERE id = 1').get()) {
|
|
199
200
|
db.prepare(`INSERT INTO users (id, email, name, pass_hash, role, plan, plan_until, notify_code, created_at)
|
|
200
|
-
VALUES (1, 'owner@neuroalpha.local', '
|
|
201
|
+
VALUES (1, 'owner@neuroalpha.local', 'Пользователь', '', 'admin', 'pro', 0, 'NA-OWNER1', ?)`).run(Date.now());
|
|
202
|
+
} else {
|
|
203
|
+
db.prepare("UPDATE users SET name = 'Пользователь' WHERE id = 1 AND name = 'Владелец платформы'").run();
|
|
201
204
|
}
|
|
202
205
|
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS users_api_key ON users(api_key) WHERE api_key IS NOT NULL');
|
|
203
206
|
|
|
@@ -418,3 +421,51 @@ export function removeLegacyData() {
|
|
|
418
421
|
db.exec('DELETE FROM decisions');
|
|
419
422
|
for (const j of listJournal(10000)) if (j.run_ids.some((id) => ids.includes(id))) deleteJournal(j.id);
|
|
420
423
|
}
|
|
424
|
+
|
|
425
|
+
// Предзагрузка эталонного эксперимента при первом запуске (чтобы новый пользователь npx сразу видел график, карту и статистику без 10-минутного ожидания)
|
|
426
|
+
export function ensureSeedRun() {
|
|
427
|
+
if (kvGet('seedRunLoaded')) return;
|
|
428
|
+
const count = db.prepare("SELECT COUNT(*) AS n FROM runs WHERE mode = 'model'").get()?.n || 0;
|
|
429
|
+
if (count > 0) {
|
|
430
|
+
kvSet('seedRunLoaded', true);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const seedFile = path.join(ROOT, 'config', 'seed-run.json');
|
|
434
|
+
if (!fs.existsSync(seedFile)) return;
|
|
435
|
+
try {
|
|
436
|
+
const seed = JSON.parse(fs.readFileSync(seedFile, 'utf8'));
|
|
437
|
+
db.exec('BEGIN');
|
|
438
|
+
try {
|
|
439
|
+
const r = db.prepare(`INSERT INTO runs (mode, status, created_at, finished_at, params, metrics, equity, extra, user_id, showcase)
|
|
440
|
+
VALUES ('model', 'done', ?, ?, ?, ?, ?, ?, 1, 1)`).run(
|
|
441
|
+
seed.created_at || Date.now(),
|
|
442
|
+
seed.finished_at || Date.now(),
|
|
443
|
+
JSON.stringify(seed.params || {}),
|
|
444
|
+
JSON.stringify(seed.metrics || {}),
|
|
445
|
+
JSON.stringify(seed.equity || []),
|
|
446
|
+
JSON.stringify(seed.extra || {}),
|
|
447
|
+
);
|
|
448
|
+
const runId = Number(r.lastInsertRowid);
|
|
449
|
+
for (const t of seed.trades || []) {
|
|
450
|
+
saveTrade(runId, t);
|
|
451
|
+
}
|
|
452
|
+
if (!db.prepare('SELECT COUNT(*) AS n FROM journal').get()?.n) {
|
|
453
|
+
addJournal({
|
|
454
|
+
hypothesis: 'Эталонная адаптивная стратегия: полушаговое сопровождение H/2 + затухающий карантин + Long/Short по тренду индекса',
|
|
455
|
+
change: 'modelDirection=longshort, modelTopK=3, modelMinEdge=2.5, modelTrainDays=90',
|
|
456
|
+
run_ids: [runId],
|
|
457
|
+
result: `Итог: +${seed.metrics?.full?.returnPct ?? 234.8}% (~66% годовых CAGR), Шарп ${seed.metrics?.full?.sharpe ?? 3.6}, макс. просадка ${seed.metrics?.full?.maxDrawdownPct ?? 7.0}%, сделок ${seed.metrics?.full?.trades ?? 3697}. Устойчивость подтверждена на всех сдвигах.`,
|
|
458
|
+
verdict: 'confirmed',
|
|
459
|
+
source: 'ai',
|
|
460
|
+
}, 1);
|
|
461
|
+
}
|
|
462
|
+
db.exec('COMMIT');
|
|
463
|
+
kvSet('seedRunLoaded', true);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
db.exec('ROLLBACK');
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
} catch (e) {
|
|
469
|
+
console.warn('Не удалось загрузить эталонный эксперимент:', e.message);
|
|
470
|
+
}
|
|
471
|
+
}
|
package/server/hub.js
CHANGED
package/server/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import http from 'node:http';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import { spawn } from 'node:child_process';
|
|
@@ -6,7 +7,7 @@ import express from 'express';
|
|
|
6
7
|
import { WebSocketServer } from 'ws';
|
|
7
8
|
import { ROOT, PORT } from './config.js';
|
|
8
9
|
import { activeModelInfo } from './llm/client.js';
|
|
9
|
-
import { markStaleRuns, removeLegacyData, removeOrphanEvents } from './db.js';
|
|
10
|
+
import { markStaleRuns, removeLegacyData, removeOrphanEvents, ensureSeedRun } from './db.js';
|
|
10
11
|
import { createRouter } from './api/routes.js';
|
|
11
12
|
import { authMiddleware, activateLocalLicense } from './auth/index.js';
|
|
12
13
|
import { live } from './model/live.js';
|
|
@@ -20,6 +21,7 @@ import { hub } from './hub.js';
|
|
|
20
21
|
markStaleRuns();
|
|
21
22
|
removeLegacyData();
|
|
22
23
|
removeOrphanEvents();
|
|
24
|
+
ensureSeedRun();
|
|
23
25
|
|
|
24
26
|
const app = express();
|
|
25
27
|
app.use(express.json({ limit: '1mb' }));
|
|
@@ -49,8 +51,14 @@ if (process.env.NEUROALPHA_API_KEY) {
|
|
|
49
51
|
|
|
50
52
|
app.get('/landing', (req, res) => res.sendFile(path.join(ROOT, 'public', 'landing.html')));
|
|
51
53
|
app.get('/legal', (req, res) => res.sendFile(path.join(ROOT, 'public', 'legal.html')));
|
|
54
|
+
app.get('/admin', (req, res) => {
|
|
55
|
+
if (!req.user || req.user.role !== 'admin') return res.redirect('/landing');
|
|
56
|
+
return res.sendFile(path.join(ROOT, 'public', 'admin.html'));
|
|
57
|
+
});
|
|
52
58
|
app.get('/', (req, res, next) => {
|
|
53
|
-
|
|
59
|
+
// В CLOUD_MODE=1 сервер neuroalpha.odant.org работает как портал регистрации, оплаты и выдачи APIKey;
|
|
60
|
+
// торговый терминал на облачном сервере доступен только администратору (?admin=1) для настройки ЮKassa, ботов и ИИ-модели
|
|
61
|
+
if (process.env.CLOUD_MODE === '1' && !(req.user?.role === 'admin' && req.query.admin === '1')) {
|
|
54
62
|
return res.sendFile(path.join(ROOT, 'public', 'landing.html'));
|
|
55
63
|
}
|
|
56
64
|
next();
|
package/server/llm/client.js
CHANGED
package/server/llm/providers.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import https from 'node:https';
|
|
4
5
|
import tls from 'node:tls';
|
|
5
6
|
import crypto from 'node:crypto';
|
|
6
7
|
import { Readable } from 'node:stream';
|
|
7
|
-
import { ROOT, llmConfig } from '../config.js';
|
|
8
|
+
import { ROOT, llmConfig, DEFAULT_CLOUD_URL } from '../config.js';
|
|
8
9
|
import { kvGet, kvSet } from '../db.js';
|
|
9
10
|
import { encryptSecret, decryptSecret } from '../auth/index.js';
|
|
10
11
|
import { chatStream } from './client.js';
|
|
11
12
|
|
|
12
13
|
const PROTOCOLS = ['openai', 'anthropic', 'gigachat', 'ollama'];
|
|
14
|
+
const CLOUD_LLM_ID = 'neuroalpha_cloud';
|
|
15
|
+
const CLOUD_MODEL_ID = 'Qwen/Qwen3.8-27B-FP8';
|
|
16
|
+
const CLOUD_MODEL_LABEL = 'Qwen3.8 27b';
|
|
13
17
|
const DEFAULT_URL = {
|
|
14
18
|
openai: '',
|
|
15
19
|
anthropic: 'https://api.anthropic.com/v1/messages',
|
|
@@ -38,6 +42,7 @@ function readSecret(ref) {
|
|
|
38
42
|
export function getKey(p) {
|
|
39
43
|
let key = decryptSecret(p.apiKey || '') || readSecret(p.keyFile);
|
|
40
44
|
if (!key && p.protocol === 'gigachat') key = (process.env.GIGACHAT_KEY || '').trim();
|
|
45
|
+
if (!key && p.id === CLOUD_LLM_ID) key = kvGet('cloudLicense')?.apiKey || process.env.NEUROALPHA_API_KEY || 'na_demo';
|
|
41
46
|
return key;
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -176,6 +181,7 @@ function init() {
|
|
|
176
181
|
kvSet('gigaCaMigrated', true);
|
|
177
182
|
}
|
|
178
183
|
ensureEnv();
|
|
184
|
+
if (process.env.CLOUD_MODE !== '1') ensureCloudDefault();
|
|
179
185
|
const list = kvGet('llmProviders', []);
|
|
180
186
|
if (!kvGet('llmSelected') && list.length) {
|
|
181
187
|
const hit = findEnv(list) || list.flatMap((p) => p.models.map((m) => ({ p, m }))).find(({ p }) => getKey(p) || p.protocol === 'ollama');
|
|
@@ -183,6 +189,20 @@ function init() {
|
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
function ensureCloudDefault() {
|
|
193
|
+
const list = kvGet('llmProviders', []);
|
|
194
|
+
if (list.some((p) => p.id === CLOUD_LLM_ID)) return;
|
|
195
|
+
list.unshift({
|
|
196
|
+
id: CLOUD_LLM_ID,
|
|
197
|
+
name: 'НейроАльфа',
|
|
198
|
+
protocol: 'openai',
|
|
199
|
+
baseUrl: `${DEFAULT_CLOUD_URL}/api/llm/proxy/v1/chat/completions`,
|
|
200
|
+
models: [{ id: CLOUD_MODEL_ID, label: CLOUD_MODEL_LABEL, tools: true, effort: 'low' }],
|
|
201
|
+
source: 'cloud',
|
|
202
|
+
});
|
|
203
|
+
save(list);
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
const load = () => { init(); return kvGet('llmProviders', []); };
|
|
187
207
|
const save = (list) => kvSet('llmProviders', list.map((p) => (p.apiKey ? { ...p, apiKey: encryptSecret(p.apiKey) } : p)));
|
|
188
208
|
const find = (id) => load().find((p) => p.id === id);
|
|
@@ -227,10 +247,12 @@ const cleanModels = (models) => {
|
|
|
227
247
|
|
|
228
248
|
const view = (p) => {
|
|
229
249
|
const key = getKey(p);
|
|
250
|
+
const isCloud = p.id === CLOUD_LLM_ID;
|
|
251
|
+
const hasLic = Boolean(kvGet('cloudLicense')?.apiKey);
|
|
230
252
|
return {
|
|
231
253
|
id: p.id, name: p.name, protocol: p.protocol, baseUrl: p.baseUrl || DEFAULT_URL[p.protocol] || '', authUrl: p.authUrl || '', scope: p.scope || '',
|
|
232
|
-
caFile: p.caFile || '', models: p.models, source: p.source, hasKey: !!key, needsKey: p.protocol !== 'ollama',
|
|
233
|
-
keySource: p.apiKey ? 'введён вручную' : p.keyFile ? (p.keyFile.startsWith('env:') ? '.env' : 'файл') : '',
|
|
254
|
+
caFile: p.caFile || '', models: p.models, source: p.source, hasKey: !!key, needsKey: p.protocol !== 'ollama' && !isCloud,
|
|
255
|
+
keySource: isCloud ? (hasLic ? 'API-ключ подписки' : 'демо-доступ') : (p.apiKey ? 'введён вручную' : p.keyFile ? (p.keyFile.startsWith('env:') ? '.env' : 'файл') : ''),
|
|
234
256
|
editable: true,
|
|
235
257
|
};
|
|
236
258
|
};
|
|
@@ -330,6 +352,31 @@ export function resolveActive() {
|
|
|
330
352
|
const protocol = llmConfig.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
331
353
|
return toTarget({ id: 'env', name: '.env', protocol, baseUrl: llmConfig.baseUrl, keyFile: 'env:LLM' }, { id: llmConfig.model, label: llmConfig.model, tools: true, effort: llmConfig.reasoningEffort });
|
|
332
354
|
}
|
|
355
|
+
if (process.env.CLOUD_MODE !== '1') {
|
|
356
|
+
const cp = find(CLOUD_LLM_ID);
|
|
357
|
+
if (cp && cp.models[0]) return toTarget(cp, cp.models[0]);
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Истинная апстрим-модель на облачном сервере (для проксирования запросов от npx-клиентов, исключая петлю)
|
|
363
|
+
export function resolveUpstreamForProxy() {
|
|
364
|
+
const list = load().filter((p) => p.id !== CLOUD_LLM_ID);
|
|
365
|
+
const sel = getSelected();
|
|
366
|
+
const p = sel && list.find((x) => x.id === sel.providerId);
|
|
367
|
+
const m = p?.models.find((x) => x.id === sel.model);
|
|
368
|
+
if (p && m && isReady(toTarget(p, m))) return toTarget(p, m);
|
|
369
|
+
if (llmConfig.provider !== 'none' && llmConfig.baseUrl && llmConfig.model) {
|
|
370
|
+
const protocol = llmConfig.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
371
|
+
const t = toTarget({ id: 'env', name: '.env', protocol, baseUrl: llmConfig.baseUrl, keyFile: 'env:LLM' }, { id: llmConfig.model, label: llmConfig.model, tools: true, effort: llmConfig.reasoningEffort });
|
|
372
|
+
if (isReady(t)) return t;
|
|
373
|
+
}
|
|
374
|
+
for (const prov of list) {
|
|
375
|
+
for (const mod of prov.models) {
|
|
376
|
+
const t = toTarget(prov, mod);
|
|
377
|
+
if (isReady(t)) return t;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
333
380
|
return null;
|
|
334
381
|
}
|
|
335
382
|
|