neuroalpha 0.0.0-stage → 0.2.0

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 (46) hide show
  1. package/README.md +164 -3
  2. package/bin/cli.js +69 -0
  3. package/config/strategy.json +33 -0
  4. package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +156 -0
  5. package/package.json +43 -6
  6. package/public/app.js +2410 -0
  7. package/public/index.html +337 -0
  8. package/public/landing.html +870 -0
  9. package/public/legal.html +90 -0
  10. package/public/styles.css +343 -0
  11. package/server/api/routes.js +333 -0
  12. package/server/auth/index.js +340 -0
  13. package/server/billing/yookassa.js +132 -0
  14. package/server/brokers/index.js +645 -0
  15. package/server/config.js +44 -0
  16. package/server/data/moex.js +165 -0
  17. package/server/db.js +420 -0
  18. package/server/hub.js +14 -0
  19. package/server/index.js +92 -0
  20. package/server/llm/client.js +187 -0
  21. package/server/llm/providers.js +355 -0
  22. package/server/model/dataset.js +352 -0
  23. package/server/model/forecast.js +121 -0
  24. package/server/model/gpu-worker.html +11 -0
  25. package/server/model/gpu-worker.js +60 -0
  26. package/server/model/linalg.js +142 -0
  27. package/server/model/live.js +486 -0
  28. package/server/model/metrics.js +134 -0
  29. package/server/model/models.js +459 -0
  30. package/server/model/profiles.js +166 -0
  31. package/server/model/research.js +880 -0
  32. package/server/model/tf-node.js +26 -0
  33. package/server/model/transformer-core.js +330 -0
  34. package/server/model/transformer-gpu.js +221 -0
  35. package/server/model/transformer-pool.js +200 -0
  36. package/server/model/transformer-worker.js +28 -0
  37. package/server/model/transformer.js +57 -0
  38. package/server/model/vocab.js +28 -0
  39. package/server/news/index.js +309 -0
  40. package/server/notify/index.js +168 -0
  41. package/server/params-meta.js +63 -0
  42. package/server/settings.js +79 -0
  43. package/server/strategist/analytics.js +150 -0
  44. package/server/strategist/index.js +484 -0
  45. package/server/strategist/tools.js +302 -0
  46. package/server/util.js +9 -0
@@ -0,0 +1,302 @@
1
+ import { PARAMS, PARAM_MAP } from '../params-meta.js';
2
+ import { getSettings, updateSettings, validateChanges } from '../settings.js';
3
+ import { getRun, listRuns, addJournal, listJournal, kvGet, kvSet } from '../db.js';
4
+ import { analyzeRun, compareRuns, recentRuns, runSummary, compactMetrics } from './analytics.js';
5
+ import { hub } from '../hub.js';
6
+ import { startModelResearch } from '../model/research.js';
7
+ import { forecastNow } from '../model/forecast.js';
8
+ import { live } from '../model/live.js';
9
+ import { listNews } from '../news/index.js';
10
+ import { plural, fmtBarMsk } from '../util.js';
11
+
12
+ export const OOS_LIMIT = 3;
13
+ const AI_LOCKED = PARAMS.filter((p) => p.ai === false).map((p) => p.key);
14
+
15
+ const changesSchema = {
16
+ type: 'object',
17
+ description: 'Изменения настроек: ключ — точное название настройки из списка в системной инструкции (например modelHorizon, modelMinEdge), значение — новое значение.',
18
+ properties: Object.fromEntries(PARAMS.filter((p) => p.ai !== false).map((p) => [p.key, {
19
+ type: p.type === 'checkbox' ? 'boolean' : p.type === 'select' ? 'string' : 'number',
20
+ description: `${p.label}${p.min !== undefined ? ` (${p.min}–${p.max})` : ''}`,
21
+ }])),
22
+ additionalProperties: false,
23
+ };
24
+
25
+ function checkChanges(changes) {
26
+ const locked = Object.keys(changes || {}).filter((k) => AI_LOCKED.includes(k));
27
+ const { values, errors } = validateChanges(changes);
28
+ for (const k of locked) { errors.push(`${k}: эту настройку ИИ менять нельзя (она отвечает за честность проверки)`); delete values[k]; }
29
+ return { values, errors };
30
+ }
31
+
32
+ const describeChanges = (values) => Object.entries(values).map(([k, v]) => `${PARAM_MAP[k]?.label || k}: ${v}`).join('; ') || 'без изменений';
33
+ const fmtPct = (v) => (v === null || v === undefined ? '—' : `${v > 0 ? '+' : ''}${v}%`);
34
+ const metricLine = (m) => (m && !m.trades ? 'сделок не было' : m ? `${m.trades} ${plural(m.trades, 'сделка', 'сделки', 'сделок')}, итог ${fmtPct(m.returnPct)}, на 1 ₽ убытка ${m.profitFactor ?? '—'} ₽, худшее падение ${m.maxDrawdownPct}%` : 'нет данных');
35
+
36
+ // Последнее исследование с текущими настройками (база для сравнения)
37
+ function findBaseline(excludeId) {
38
+ const cur = getSettings();
39
+ const keys = PARAMS.map((p) => p.key).filter((k) => !['capital', 'paperDailyReview', 'paperPollSec', 'autopilotBudget'].includes(k));
40
+ return listRuns().find((r) => r.id !== excludeId && r.mode === 'model' && r.status === 'done'
41
+ && keys.every((k) => JSON.stringify(r.params?.[k] ?? null) === JSON.stringify(cur[k] ?? null))) || null;
42
+ }
43
+
44
+ // kind: read — без подтверждения; write — подтверждение в режиме «Советник»; danger — подтверждение всегда;
45
+ // confirmAlways — подтверждение во всех режимах (кроме read-режимов, где инструмент недоступен)
46
+ export const TOOLS = {
47
+ get_settings: {
48
+ kind: 'read',
49
+ label: 'Смотрит текущие настройки',
50
+ description: 'Текущие настройки модели и счёта с пояснениями.',
51
+ parameters: { type: 'object', properties: {} },
52
+ async run() {
53
+ const s = getSettings();
54
+ return {
55
+ result: Object.fromEntries(PARAMS.map((p) => [p.key, { значение: s[p.key], что_это: p.label, пояснение: p.hint, ...(p.ai === false ? { менять_нельзя: true } : {}) }])),
56
+ summary: 'Прочитал текущие настройки',
57
+ };
58
+ },
59
+ },
60
+
61
+ list_runs: {
62
+ kind: 'read',
63
+ label: 'Смотрит список исследований',
64
+ description: 'Последние исследования модели и живые проверки с результатами на периоде настройки.',
65
+ parameters: { type: 'object', properties: { limit: { type: 'number', description: 'сколько последних, по умолчанию 10' } } },
66
+ async run({ limit = 10 }) {
67
+ return { result: recentRuns(Math.min(30, limit)), summary: `Просмотрел последние исследования` };
68
+ },
69
+ },
70
+
71
+ analyze_run: {
72
+ kind: 'read',
73
+ label: 'Разбирает исследование',
74
+ description: 'Подробный разбор исследования: сравнение моделей, сделки торгующей модели по акциям, часу входа, сроку удержания, веса голов внимания. Только период настройки.',
75
+ parameters: { type: 'object', properties: { run_id: { type: 'number', description: 'номер исследования' } }, required: ['run_id'] },
76
+ async run({ run_id }) {
77
+ const r = analyzeRun(Number(run_id));
78
+ return { result: r, summary: r.ошибка || `Разобрал исследование №${run_id}` };
79
+ },
80
+ },
81
+
82
+ compare_runs: {
83
+ kind: 'read',
84
+ label: 'Сравнивает исследования',
85
+ description: 'Сравнить два исследования: чем отличаются настройки и результаты на периоде настройки.',
86
+ parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] },
87
+ async run({ a, b }) {
88
+ return { result: compareRuns(Number(a), Number(b)), summary: `Сравнил исследования №${a} и №${b}` };
89
+ },
90
+ },
91
+
92
+ run_model_research: {
93
+ kind: 'write',
94
+ budget: true,
95
+ label: 'Исследует модель',
96
+ description: 'Пошаговая проверка трёх моделей предсказания «следующего состояния рынка» (~100 акций, часовые свечи): своя инерция, кто за кем ходит, внимание через карту рынка (и трансформер, если modelUseTransformer=true — тогда в разы дольше). Модель учится на окне прошлого, прогнозирует следующий месяц, окно сдвигается. Возвращает сравнение моделей на периоде настройки и базу для сравнения. Можно передать изменения настроек (сами настройки не меняются). Около минуты.',
97
+ parameters: { type: 'object', properties: { changes: changesSchema, note: { type: 'string', description: 'что проверяем (гипотеза)' } }, required: ['note'] },
98
+ preview(args) {
99
+ const { values, errors } = checkChanges(args.changes);
100
+ return `${args.note || 'Исследование модели'}\nИзменения: ${describeChanges(values)}${errors.length ? `\nОшибки: ${errors.join('; ')}` : ''}`;
101
+ },
102
+ async run(args, ctx) {
103
+ const { values, errors } = checkChanges(args.changes);
104
+ if (errors.length) return { result: { ошибка: errors }, summary: `Ошибка в изменениях: ${errors.join('; ')}`, ok: false };
105
+ const { runId, done, reused } = startModelResearch({ ...getSettings(), ...values }, { source: 'ai', note: args.note || '' });
106
+ ctx.onRunStart?.(runId);
107
+ try { await done; } catch (e) {
108
+ return { result: { исследование: runId, ошибка: e.message }, summary: `Исследование №${runId}: ${e.message}`, ok: false };
109
+ }
110
+ const run = getRun(runId);
111
+ const base = findBaseline(runId);
112
+ const c = run.metrics?.model?.comparison?.attention?.inSample;
113
+ return {
114
+ result: {
115
+ ...runSummary(run),
116
+ изменения: values,
117
+ ...(reused ? { повтор: `результат взят из исследования №${runId} с теми же настройками` } : {}),
118
+ база_для_сравнения: base ? runSummary(base) : 'нет исследования с текущими настройками без изменений — для честного сравнения запусти run_model_research без changes',
119
+ },
120
+ summary: reused
121
+ ? `Исследование №${runId} (готовый результат): внимание через карту — совпадение ${c?.ic}, итог ${fmtPct(c?.returnPct)} (без комиссий ${fmtPct(c?.grossReturnPct)}), ${c?.trades} сделок (период настройки)`
122
+ : `Исследование №${runId}: внимание через карту — совпадение ${c?.ic}, итог ${fmtPct(c?.returnPct)} (без комиссий ${fmtPct(c?.grossReturnPct)}), ${c?.trades} сделок (период настройки)`,
123
+ runId,
124
+ };
125
+ },
126
+ },
127
+
128
+ model_forecast: {
129
+ kind: 'read',
130
+ label: 'Смотрит прогноз на ближайшие часы',
131
+ description: 'Прогноз модели по последней закрытой свече: какие акции, по её мнению, будут сильнее или слабее рынка в ближайшие часы и под чьим влиянием (соседи по карте).',
132
+ parameters: { type: 'object', properties: { top: { type: 'number', description: 'сколько акций показать, по умолчанию 10' } } },
133
+ async run({ top = 10 }) {
134
+ const r = await forecastNow(getSettings());
135
+ const pick = (x) => ({ акция: x.symbol, ожидание_проц: x.expectedPct, сигнал_покупки: !!x.signal, ...(x.shortSignal !== undefined ? { сигнал_продажи: !!x.shortSignal } : {}), ликвидна: x.liquid, последний_час_проц: x.lastMovePct, влияют: x.influences.map((n) => `${n.symbol} (${n.contribPct > 0 ? '+' : ''}${n.contribPct}%, близость ${n.closeness})`), своё_движение_проц: x.selfPct });
136
+ return {
137
+ result: { свеча: r.bar, горизонт_часов: r.horizon, порог_покупки_проц: r.thresholdPct, сильнее_рынка: r.rows.slice(0, top).map(pick), слабее_рынка: r.rows.slice(-Math.min(5, top)).map(pick) },
138
+ summary: `Прогноз на свече ${r.bar}: сигналов покупки ${r.rows.filter((x) => x.signal).length}`,
139
+ };
140
+ },
141
+ },
142
+
143
+ market_neighbors: {
144
+ kind: 'read',
145
+ label: 'Смотрит соседей на карте',
146
+ description: 'Ближайшие соседи акции на карте рынка (кто движется похоже) и её группа — по последнему исследованию.',
147
+ parameters: { type: 'object', properties: { symbol: { type: 'string' } }, required: ['symbol'] },
148
+ async run({ symbol }) {
149
+ const run = listRuns().find((r) => r.mode === 'model' && r.status === 'done');
150
+ const ex = run && getRun(run.id).extra;
151
+ if (!ex) return { result: { ошибка: 'нет исследования — запусти run_model_research' }, summary: 'Нет исследования', ok: false };
152
+ const i = ex.symbols.indexOf(String(symbol).toUpperCase());
153
+ if (i < 0) return { result: { ошибка: `${symbol} нет в словаре модели` }, summary: `${symbol} нет в словаре`, ok: false };
154
+ const snap = ex.map.snapshots.at(-1);
155
+ return {
156
+ result: { акция: ex.symbols[i], соседи: ex.neighbors[i].map(([k, c]) => ({ акция: ex.symbols[k], близость: c })), группа_на_карте: ex.symbols.filter((_, k) => snap.cl[k] === snap.cl[i]) },
157
+ summary: `Соседи ${ex.symbols[i]}: ${ex.neighbors[i].slice(0, 4).map(([k]) => ex.symbols[k]).join(', ')}`,
158
+ };
159
+ },
160
+ },
161
+
162
+ get_news: {
163
+ kind: 'read',
164
+ label: 'Смотрит свежие новости',
165
+ description: 'Свежие новости и раскрытия эмитентов: каких акций из словаря касаются, тон −1…1 (плохо/хорошо для акции), важность 0…1 и короткая суть. Без symbol — все важные новости за период. Новости есть только «сейчас»: в исторических проверках они не используются.',
166
+ parameters: {
167
+ type: 'object',
168
+ properties: {
169
+ symbol: { type: 'string', description: 'тикер, например SBER; без него — все важные новости' },
170
+ hours: { type: 'number', description: 'за сколько часов смотреть, по умолчанию 24' },
171
+ },
172
+ },
173
+ async run({ symbol, hours = 24 }) {
174
+ const items = listNews({ symbol, hours, limit: 25 });
175
+ if (!items.length) return { result: { новостей: 'нет' }, summary: `Новостей ${symbol ? `по ${symbol}` : ''} за последние ${hours} ч нет` };
176
+ return {
177
+ result: items.map((n) => ({
178
+ время: fmtBarMsk(n.ts), источник: n.src,
179
+ ...(n.symbols.length ? { акции: n.symbols.map((s) => `${s.symbol} (тон ${s.tone ?? '—'})`) } : {}),
180
+ ...(n.imp !== null ? { важность: n.imp } : {}), суть: n.gist || n.title, ссылка: n.link,
181
+ })),
182
+ summary: `Новости за ${hours} ч: ${items.length}${symbol ? ` по ${symbol}` : ''}`,
183
+ };
184
+ },
185
+ },
186
+
187
+ journal_list: {
188
+ kind: 'read',
189
+ label: 'Читает журнал гипотез',
190
+ description: 'Полный журнал гипотез: что уже проверяли и к каким выводам пришли.',
191
+ parameters: { type: 'object', properties: {} },
192
+ async run() {
193
+ return { result: listJournal(100), summary: 'Прочитал журнал гипотез' };
194
+ },
195
+ },
196
+
197
+ journal_add: {
198
+ kind: 'read',
199
+ label: 'Записывает вывод в журнал',
200
+ description: 'Записать в журнал гипотез результат эксперимента. Делай это после каждого законченного эксперимента.',
201
+ parameters: {
202
+ type: 'object',
203
+ properties: {
204
+ hypothesis: { type: 'string', description: 'что предполагали, простыми словами' },
205
+ change: { type: 'string', description: 'что меняли' },
206
+ run_ids: { type: 'array', items: { type: 'number' }, description: 'номера исследований: базовое и новое' },
207
+ result: { type: 'string', description: 'что получилось, с цифрами и числом сделок' },
208
+ verdict: { type: 'string', enum: ['confirmed', 'rejected', 'unclear'], description: 'подтвердилась / не подтвердилась / неясно' },
209
+ },
210
+ required: ['hypothesis', 'result', 'verdict'],
211
+ },
212
+ async run(args) {
213
+ const id = addJournal({ ...args, source: 'ai' });
214
+ hub.emit({ type: 'journal:update' });
215
+ const v = { confirmed: 'подтвердилась', rejected: 'не подтвердилась', unclear: 'неясно' }[args.verdict] || args.verdict;
216
+ return { result: { ok: true, id }, summary: `Записал в журнал: «${args.hypothesis}» — ${v}` };
217
+ },
218
+ },
219
+
220
+ final_check: {
221
+ kind: 'read',
222
+ label: 'Смотрит контрольный период',
223
+ description: `Открыть результат исследования на КОНТРОЛЬНОМ периоде (данные, которые не используются для подбора). Не больше ${OOS_LIMIT} раз за разговор — только для финальной оценки лучшего варианта против базы.`,
224
+ parameters: { type: 'object', properties: { run_id: { type: 'number' } }, required: ['run_id'] },
225
+ async run({ run_id }, ctx) {
226
+ const key = `oos_used_${ctx.convId}`;
227
+ const used = kvGet(key, []);
228
+ if (!used.includes(Number(run_id)) && used.length >= OOS_LIMIT) {
229
+ return { result: { ошибка: `лимит ${OOS_LIMIT} просмотров контрольного периода за разговор исчерпан` }, summary: 'Лимит просмотров контрольного периода исчерпан', ok: false };
230
+ }
231
+ const run = getRun(Number(run_id));
232
+ if (!run) return { result: { ошибка: 'нет такого исследования' }, summary: 'Нет такого исследования', ok: false };
233
+ if (!used.includes(Number(run_id))) kvSet(key, [...used, Number(run_id)]);
234
+ return {
235
+ result: { ...runSummary(run, { revealOOS: true }), осталось_просмотров: OOS_LIMIT - kvGet(key, []).length },
236
+ summary: `Контрольный период №${run_id}: ${metricLine(run.metrics?.outOfSample)}`,
237
+ };
238
+ },
239
+ },
240
+
241
+ set_settings: {
242
+ kind: 'write',
243
+ confirmAlways: true,
244
+ label: 'Меняет настройки',
245
+ description: 'Сохранить изменения в рабочие настройки (их видит пользователь и использует живая проверка). Только когда изменение подтверждено исследованиями.',
246
+ parameters: { type: 'object', properties: { changes: changesSchema, reason: { type: 'string', description: 'почему' } }, required: ['changes', 'reason'] },
247
+ preview(args) {
248
+ const { values, errors } = checkChanges(args.changes);
249
+ return `${args.reason || ''}\nИзменения: ${describeChanges(values)}${errors.length ? `\nОшибки: ${errors.join('; ')}` : ''}`;
250
+ },
251
+ async run(args) {
252
+ const { values, errors } = checkChanges(args.changes);
253
+ const r = updateSettings(values, { source: 'ai', reason: args.reason });
254
+ const all = [...errors, ...r.errors];
255
+ return {
256
+ result: { изменено: r.changed, ошибки: all.length ? all : undefined },
257
+ summary: Object.keys(r.changed).length ? `Настройки изменены: ${describeChanges(Object.fromEntries(Object.entries(r.changed).map(([k, v]) => [k, v.стало])))}` : `Настройки не изменились${all.length ? `: ${all.join('; ')}` : ''}`,
258
+ ok: !all.length,
259
+ };
260
+ },
261
+ },
262
+
263
+ paper_start: {
264
+ kind: 'danger',
265
+ label: 'Запускает живую проверку',
266
+ description: 'Запустить живую проверку модели (виртуальная торговля на текущих ценах) с текущими настройками.',
267
+ parameters: { type: 'object', properties: { reason: { type: 'string' } } },
268
+ preview: (args) => args.reason || 'Запустить живую проверку с текущими настройками',
269
+ async run() {
270
+ if (live.status().running) return { result: { ошибка: 'уже запущена' }, summary: 'Живая проверка уже запущена', ok: false };
271
+ const runId = await live.start(getSettings(), { source: 'ai' });
272
+ return { result: { ok: true, проверка: runId }, summary: `Живая проверка №${runId} запущена` };
273
+ },
274
+ },
275
+
276
+ paper_stop: {
277
+ kind: 'danger',
278
+ label: 'Останавливает живую проверку',
279
+ description: 'Остановить живую проверку.',
280
+ parameters: { type: 'object', properties: { reason: { type: 'string' } } },
281
+ preview: (args) => args.reason || 'Остановить живую проверку',
282
+ async run() {
283
+ const st = live.status();
284
+ if (!st.running) return { result: { ошибка: 'не запущена' }, summary: 'Живая проверка не запущена', ok: false };
285
+ live.stop();
286
+ return { result: { ok: true, итог: compactMetrics(getRun(st.runId)?.metrics?.full) }, summary: `Живая проверка №${st.runId} остановлена` };
287
+ },
288
+ },
289
+ };
290
+
291
+ export const MODES = {
292
+ discuss: { label: 'Обсуждение', allowed: (t) => t.kind === 'read' },
293
+ advisor: { label: 'Советник', allowed: () => true, confirm: (t) => t.kind !== 'read' },
294
+ autopilot: { label: 'Автопилот', allowed: () => true, confirm: (t) => t.kind === 'danger' || !!t.confirmAlways },
295
+ review: { label: 'Разбор дня', allowed: (t) => t.kind === 'read' },
296
+ };
297
+
298
+ export function toolSchemas(mode) {
299
+ return Object.entries(TOOLS)
300
+ .filter(([, t]) => MODES[mode].allowed(t))
301
+ .map(([name, t]) => ({ type: 'function', function: { name, description: t.description, parameters: t.parameters } }));
302
+ }
package/server/util.js ADDED
@@ -0,0 +1,9 @@
1
+ export const DAY = 86400000;
2
+ export const MSK = 3 * 3600000;
3
+ export const round = (v, d = 2) => (Number.isFinite(v) ? Math.round(v * 10 ** d) / 10 ** d : v);
4
+ export const fmtDayMsk = (ts) => new Date(ts + MSK).toISOString().slice(0, 10).split('-').reverse().join('.');
5
+ export const fmtBarMsk = (ts) => new Date(ts + MSK).toISOString().slice(0, 16).replace('T', ' ');
6
+ export const plural = (n, one, few, many) => {
7
+ const m10 = n % 10, m100 = n % 100;
8
+ return m10 === 1 && m100 !== 11 ? one : m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20) ? few : many;
9
+ };