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.
Files changed (52) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +9 -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 +449 -0
  12. package/public/app.js +1182 -379
  13. package/public/index.html +87 -22
  14. package/public/landing.html +1217 -870
  15. package/public/legal.html +51 -8
  16. package/public/styles.css +71 -16
  17. package/server/api/routes.js +220 -35
  18. package/server/auth/index.js +633 -340
  19. package/server/billing/yookassa.js +1 -0
  20. package/server/brokers/index.js +1369 -645
  21. package/server/config.js +6 -4
  22. package/server/data/moex.js +1 -0
  23. package/server/db.js +69 -4
  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 +202 -37
  30. package/server/model/linalg.js +1 -0
  31. package/server/model/live.js +476 -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 +128 -205
  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 +22 -26
  40. package/server/settings.js +1 -0
  41. package/server/strategist/analytics.js +2 -1
  42. package/server/strategist/index.js +7 -7
  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. 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 || '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. All rights reserved. Proprietary software.
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. 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
- // Гарантируем наличие аккаунта владельца (id = 1, admin)
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', 'Владелец платформы', '', 'admin', 'pro', 0, 'NA-OWNER1', ?)`).run(Date.now());
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' AND (showcase = 1 OR id IN (SELECT id FROM runs WHERE mode = 'model' AND status = 'done' ORDER BY json_extract(metrics, '$.full.returnPct') DESC LIMIT 3)) ORDER BY showcase DESC, json_extract(metrics, '$.full.returnPct') DESC LIMIT 4").all();
369
- return rows.map(parseRun);
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
@@ -1,3 +1,4 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
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. 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
- if (process.env.CLOUD_MODE === '1' && !req.user && req.query.terminal !== '1' && !req.query.run) {
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();
@@ -1,3 +1,4 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
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. 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