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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -3
  3. package/bin/cli.js +69 -69
  4. package/config/seed-run.json +1 -0
  5. package/config/strategy.json +4 -10
  6. package/deploy/install.sh +128 -0
  7. package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +103 -0
  8. 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
  9. 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
  10. package/package.json +15 -5
  11. package/public/admin.html +426 -0
  12. package/public/app.js +741 -251
  13. package/public/index.html +45 -20
  14. package/public/landing.html +1004 -870
  15. package/public/legal.html +22 -0
  16. package/public/styles.css +49 -15
  17. package/server/api/routes.js +176 -23
  18. package/server/auth/index.js +544 -340
  19. package/server/billing/yookassa.js +1 -0
  20. package/server/brokers/index.js +1224 -645
  21. package/server/config.js +6 -4
  22. package/server/data/moex.js +1 -0
  23. package/server/db.js +53 -2
  24. package/server/hub.js +1 -0
  25. package/server/index.js +10 -2
  26. package/server/llm/client.js +1 -0
  27. package/server/llm/providers.js +50 -3
  28. package/server/model/dataset.js +362 -352
  29. package/server/model/forecast.js +15 -21
  30. package/server/model/linalg.js +1 -0
  31. package/server/model/live.js +489 -486
  32. package/server/model/metrics.js +1 -0
  33. package/server/model/models.js +31 -21
  34. package/server/model/profiles.js +1 -0
  35. package/server/model/research.js +119 -172
  36. package/server/model/vocab.js +1 -0
  37. package/server/news/index.js +1 -0
  38. package/server/notify/index.js +304 -168
  39. package/server/params-meta.js +20 -25
  40. package/server/settings.js +1 -0
  41. package/server/strategist/analytics.js +1 -0
  42. package/server/strategist/index.js +6 -6
  43. package/server/strategist/tools.js +2 -1
  44. package/server/util.js +1 -0
  45. package/server/model/gpu-worker.html +0 -11
  46. package/server/model/gpu-worker.js +0 -60
  47. package/server/model/tf-node.js +0 -26
  48. package/server/model/transformer-core.js +0 -330
  49. package/server/model/transformer-gpu.js +0 -221
  50. package/server/model/transformer-pool.js +0 -200
  51. package/server/model/transformer-worker.js +0 -28
  52. 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 || 'none').toLowerCase(),
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(/\/+$/, '');
@@ -1,3 +1,4 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
2
  import { saveCandles, loadCandles, candleRange, kvGet, kvSet } from '../db.js';
2
3
  import { DAY, MSK } from '../util.js';
3
4
 
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
- // Гарантируем наличие аккаунта владельца (id = 1, admin)
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', 'Владелец платформы', '', 'admin', 'pro', 0, 'NA-OWNER1', ?)`).run(Date.now());
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
@@ -1,3 +1,4 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
2
  // Общий канал событий сервера → браузер (WebSocket) и подписчики внутри сервера
2
3
  const listeners = new Set();
3
4
  let sender = () => {};
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
- if (process.env.CLOUD_MODE === '1' && !req.user && req.query.terminal !== '1' && !req.query.run) {
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();
@@ -1,3 +1,4 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
2
  import { llmConfig } from '../config.js';
2
3
  import { resolveActive, isReady, providerFetch } from './providers.js';
3
4
 
@@ -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