neuroalpha 0.2.0 → 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.
- package/LICENSE +17 -0
- package/README.md +9 -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 +449 -0
- package/public/app.js +1182 -379
- package/public/index.html +87 -22
- package/public/landing.html +1217 -870
- package/public/legal.html +51 -8
- package/public/styles.css +71 -16
- package/server/api/routes.js +220 -35
- package/server/auth/index.js +633 -340
- package/server/billing/yookassa.js +1 -0
- package/server/brokers/index.js +1369 -645
- package/server/config.js +6 -4
- package/server/data/moex.js +1 -0
- package/server/db.js +69 -4
- 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 +202 -37
- package/server/model/linalg.js +1 -0
- package/server/model/live.js +476 -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 +128 -205
- 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 +22 -26
- package/server/settings.js +1 -0
- package/server/strategist/analytics.js +2 -1
- package/server/strategist/index.js +7 -7
- 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. All rights reserved. Proprietary software.
|
|
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. All rights reserved. Proprietary software.
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -188,16 +189,19 @@ for (const [tbl, col, def] of [
|
|
|
188
189
|
['journal', 'user_id', 'INTEGER DEFAULT 1'],
|
|
189
190
|
['settings_history', 'user_id', 'INTEGER DEFAULT 1'],
|
|
190
191
|
['users', 'api_key', 'TEXT'],
|
|
192
|
+
['users', 'offer_accepted_at', 'INTEGER DEFAULT 0'],
|
|
191
193
|
]) {
|
|
192
194
|
if (!db.prepare(`PRAGMA table_info(${tbl})`).all().some((c) => c.name === col)) {
|
|
193
195
|
db.exec(`ALTER TABLE ${tbl} ADD COLUMN ${col} ${def}`);
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
// Гарантируем наличие
|
|
199
|
+
// Гарантируем наличие локального пользователя по умолчанию (id = 1)
|
|
198
200
|
if (!db.prepare('SELECT id FROM users WHERE id = 1').get()) {
|
|
199
201
|
db.prepare(`INSERT INTO users (id, email, name, pass_hash, role, plan, plan_until, notify_code, created_at)
|
|
200
|
-
VALUES (1, 'owner@neuroalpha.local', '
|
|
202
|
+
VALUES (1, 'owner@neuroalpha.local', 'Пользователь', '', 'admin', 'pro', 0, 'NA-OWNER1', ?)`).run(Date.now());
|
|
203
|
+
} else {
|
|
204
|
+
db.prepare("UPDATE users SET name = 'Пользователь' WHERE id = 1 AND name = 'Владелец платформы'").run();
|
|
201
205
|
}
|
|
202
206
|
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS users_api_key ON users(api_key) WHERE api_key IS NOT NULL');
|
|
203
207
|
|
|
@@ -365,8 +369,21 @@ export function setRunShowcase(id, showcase) {
|
|
|
365
369
|
return getRun(id);
|
|
366
370
|
}
|
|
367
371
|
export function listShowcaseRuns() {
|
|
368
|
-
const rows = db.prepare("SELECT * FROM runs WHERE mode = 'model' AND status = 'done'
|
|
369
|
-
|
|
372
|
+
const rows = db.prepare("SELECT * FROM runs WHERE mode = 'model' AND status = 'done' ORDER BY showcase DESC, json_extract(metrics, '$.full.returnPct') ASC LIMIT 40").all().map(parseRun);
|
|
373
|
+
const picked = [];
|
|
374
|
+
const seenSig = new Set();
|
|
375
|
+
for (const r of rows) {
|
|
376
|
+
const dir = r.params?.modelDirection || 'long';
|
|
377
|
+
if (dir === 'neutral') continue;
|
|
378
|
+
const ret = Math.round(Number(r.metrics?.full?.returnPct || 0));
|
|
379
|
+
const topK = r.params?.modelTopK || 2;
|
|
380
|
+
const sig = `${dir}_${topK}_${ret}`;
|
|
381
|
+
if (seenSig.has(sig)) continue;
|
|
382
|
+
seenSig.add(sig);
|
|
383
|
+
picked.push(r);
|
|
384
|
+
if (picked.length >= 3) break;
|
|
385
|
+
}
|
|
386
|
+
return picked.sort((a, b) => (Number(a.metrics?.full?.returnPct || 0) - Number(b.metrics?.full?.returnPct || 0)));
|
|
370
387
|
}
|
|
371
388
|
export function countUserRunsToday(uid = getUid()) {
|
|
372
389
|
const since = Date.now() - 86400000;
|
|
@@ -418,3 +435,51 @@ export function removeLegacyData() {
|
|
|
418
435
|
db.exec('DELETE FROM decisions');
|
|
419
436
|
for (const j of listJournal(10000)) if (j.run_ids.some((id) => ids.includes(id))) deleteJournal(j.id);
|
|
420
437
|
}
|
|
438
|
+
|
|
439
|
+
// Предзагрузка эталонного эксперимента при первом запуске (чтобы новый пользователь npx сразу видел график, карту и статистику без 10-минутного ожидания)
|
|
440
|
+
export function ensureSeedRun() {
|
|
441
|
+
if (kvGet('seedRunLoaded')) return;
|
|
442
|
+
const count = db.prepare("SELECT COUNT(*) AS n FROM runs WHERE mode = 'model'").get()?.n || 0;
|
|
443
|
+
if (count > 0) {
|
|
444
|
+
kvSet('seedRunLoaded', true);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const seedFile = path.join(ROOT, 'config', 'seed-run.json');
|
|
448
|
+
if (!fs.existsSync(seedFile)) return;
|
|
449
|
+
try {
|
|
450
|
+
const seed = JSON.parse(fs.readFileSync(seedFile, 'utf8'));
|
|
451
|
+
db.exec('BEGIN');
|
|
452
|
+
try {
|
|
453
|
+
const r = db.prepare(`INSERT INTO runs (mode, status, created_at, finished_at, params, metrics, equity, extra, user_id, showcase)
|
|
454
|
+
VALUES ('model', 'done', ?, ?, ?, ?, ?, ?, 1, 1)`).run(
|
|
455
|
+
seed.created_at || Date.now(),
|
|
456
|
+
seed.finished_at || Date.now(),
|
|
457
|
+
JSON.stringify(seed.params || {}),
|
|
458
|
+
JSON.stringify(seed.metrics || {}),
|
|
459
|
+
JSON.stringify(seed.equity || []),
|
|
460
|
+
JSON.stringify(seed.extra || {}),
|
|
461
|
+
);
|
|
462
|
+
const runId = Number(r.lastInsertRowid);
|
|
463
|
+
for (const t of seed.trades || []) {
|
|
464
|
+
saveTrade(runId, t);
|
|
465
|
+
}
|
|
466
|
+
if (!db.prepare('SELECT COUNT(*) AS n FROM journal').get()?.n) {
|
|
467
|
+
addJournal({
|
|
468
|
+
hypothesis: 'Эталонная адаптивная стратегия: полушаговое сопровождение H/2 + затухающий карантин + Long/Short по тренду индекса',
|
|
469
|
+
change: 'modelDirection=longshort, modelTopK=3, modelMinEdge=2.5, modelTrainDays=90',
|
|
470
|
+
run_ids: [runId],
|
|
471
|
+
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}. Устойчивость подтверждена на всех сдвигах.`,
|
|
472
|
+
verdict: 'confirmed',
|
|
473
|
+
source: 'ai',
|
|
474
|
+
}, 1);
|
|
475
|
+
}
|
|
476
|
+
db.exec('COMMIT');
|
|
477
|
+
kvSet('seedRunLoaded', true);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
db.exec('ROLLBACK');
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
} catch (e) {
|
|
483
|
+
console.warn('Не удалось загрузить эталонный эксперимент:', e.message);
|
|
484
|
+
}
|
|
485
|
+
}
|
package/server/hub.js
CHANGED
package/server/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
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 неавторизованным гостям показывается лендинг (витрина PaaS),
|
|
60
|
+
// а авторизованным пользователям открывается их изолированный облачный терминал (или витрина при ?landing=1)
|
|
61
|
+
if (process.env.CLOUD_MODE === '1' && (!req.user || req.query.landing === '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. All rights reserved. Proprietary software.
|
|
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
|
|