neuroalpha 0.3.12 → 0.3.14

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 (41) hide show
  1. package/LICENSE +13 -17
  2. package/README.md +4 -2
  3. package/bin/cli.js +1 -1
  4. package/deploy/install.sh +1 -1
  5. 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
  6. 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 +1 -1
  7. package/package.json +2 -2
  8. package/public/admin.html +43 -20
  9. package/public/app.js +780 -226
  10. package/public/index.html +43 -3
  11. package/public/landing.html +328 -115
  12. package/public/legal.html +37 -16
  13. package/public/styles.css +24 -3
  14. package/server/api/routes.js +62 -30
  15. package/server/auth/index.js +109 -20
  16. package/server/billing/yookassa.js +1 -1
  17. package/server/brokers/index.js +256 -53
  18. package/server/config.js +1 -1
  19. package/server/data/moex.js +1 -1
  20. package/server/db.js +17 -3
  21. package/server/hub.js +1 -1
  22. package/server/index.js +4 -4
  23. package/server/llm/client.js +1 -1
  24. package/server/llm/providers.js +1 -1
  25. package/server/model/dataset.js +1 -1
  26. package/server/model/forecast.js +354 -25
  27. package/server/model/linalg.js +1 -1
  28. package/server/model/live.js +36 -25
  29. package/server/model/metrics.js +1 -1
  30. package/server/model/models.js +1 -1
  31. package/server/model/profiles.js +1 -1
  32. package/server/model/research.js +14 -38
  33. package/server/model/vocab.js +1 -1
  34. package/server/news/index.js +1 -1
  35. package/server/notify/index.js +1 -1
  36. package/server/params-meta.js +4 -3
  37. package/server/settings.js +1 -1
  38. package/server/strategist/analytics.js +2 -2
  39. package/server/strategist/index.js +2 -2
  40. package/server/strategist/tools.js +1 -1
  41. package/server/util.js +1 -1
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { normalizeParams } from '../settings.js';
3
3
  import { computeReport, downsample, buildPeriods } from './metrics.js';
4
4
  import { round } from '../util.js';
@@ -312,8 +312,8 @@ async function run(runId, p, state) {
312
312
  const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
313
313
  const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
314
314
  let curExitRatio = autoAdapt ? 0.95 : exitRatio;
315
- const stepBars = autoAdapt ? Math.max(2, Math.floor(H / 2)) : H;
316
- const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : (dir === 'neutral' ? Math.min(0.25, 1 / (2 * K)) : Math.min(0.35, 1.4 / (2 * K)));
315
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
316
+ const maxSlotWeight = dir === 'long' ? Math.min(0.48, Math.max(1 / K, 1.4 / K)) : Math.min(0.48, 1.9 / (2 * K));
317
317
 
318
318
  const closePos = (i, h, t, end, why) => {
319
319
  const exitPrice = px(i, t);
@@ -345,7 +345,7 @@ async function run(runId, p, state) {
345
345
  const pr = preds[name].get(t);
346
346
  const sg = sigmaAt.get(t);
347
347
  const bear = isMarketDown(ds, t);
348
- const mktDown = mktFilter && dir !== 'neutral' && bear;
348
+ const mktDown = mktFilter && bear;
349
349
  let order, entries;
350
350
  if (dir !== 'long') {
351
351
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
@@ -359,26 +359,12 @@ async function run(runId, p, state) {
359
359
  const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
360
360
  if (!(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn, false, 'Прогноз ослаб');
361
361
  }
362
- const needL = autoAdapt && dir === 'longshort' && bear ? need * 1.25 : need;
363
- const needS = autoAdapt && dir === 'longshort' ? (bear ? need * 0.90 : need * 1.25) : need;
362
+ const needL = autoAdapt && bear ? need * 1.25 : need;
363
+ const needS = autoAdapt ? (bear ? need * 0.90 : need * 1.25) : need;
364
364
  const candL = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]);
365
365
  const candS = [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) < -needS).sort((a, b) => pr[a] - pr[b]);
366
- let nL, nS;
367
- if (dir === 'longshort') {
368
- nL = K - countSide('long');
369
- nS = K - countSide('short');
370
- } else {
371
- // нейтрально: равное число покупок и продаж; при autoAdapt уже открытые прибыльные позиции не закрываются принудительно без сигнала
372
- const pairs = Math.min(K, countSide('long') + candL.length, countSide('short') + candS.length);
373
- if (!autoAdapt) {
374
- for (const side of ['long', 'short']) {
375
- const extra = [...held].filter(([, h]) => h.side === side).sort((a, b) => (side === 'long' ? 1 : -1) * (edge(a[0]) - edge(b[0])));
376
- for (const [i, h] of extra.slice(0, Math.max(0, extra.length - pairs))) closePos(i, h, tIn, false, 'Нет пары с другой стороны');
377
- }
378
- }
379
- nL = Math.max(0, pairs - countSide('long'));
380
- nS = Math.max(0, pairs - countSide('short'));
381
- }
366
+ const nL = K - countSide('long');
367
+ const nS = K - countSide('short');
382
368
  entries = [
383
369
  ...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long', edge(i)]),
384
370
  ...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short', edge(i)]),
@@ -419,7 +405,7 @@ async function run(runId, p, state) {
419
405
  for (let idx = 0; idx < newEntries.length; idx++) {
420
406
  const [i, side, edgeAt] = newEntries[idx];
421
407
  const effEdge = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
422
- const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.20, Math.max(0.85, 0.80 + 0.20 * (effEdge / needEdge))) : 1;
408
+ const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.25, Math.max(0.85, 0.80 + 0.25 * (effEdge / needEdge))) : 1;
423
409
  const baseAlloc = autoAdapt ? Math.min(equity * maxSlotWeight * conv, avail / Math.max(1, newEntries.length - idx)) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
424
410
  let alloc = baseAlloc;
425
411
  if (invVol && avgInv > 0 && sg[i] > 0) {
@@ -713,7 +699,7 @@ async function runStabilityLoop(targetId, runRow, st) {
713
699
  const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
714
700
  const curExitRatio = autoAdapt ? 0.95 : exitRatio;
715
701
  const stepBars = autoAdapt ? Math.max(2, Math.floor(H / 2)) : H;
716
- const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : (dir === 'neutral' ? Math.min(0.25, 1 / (2 * K)) : Math.min(0.35, 1.4 / (2 * K)));
702
+ const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : Math.min(0.35, 1.4 / (2 * K));
717
703
  const closePos = (i, h, t) => {
718
704
  const exitPrice = px(i, t);
719
705
  const gross = h.side === 'short' ? h.alloc * (1 - exitPrice / h.entryPrice) : h.alloc * (exitPrice / h.entryPrice - 1);
@@ -735,7 +721,7 @@ async function runStabilityLoop(targetId, runRow, st) {
735
721
  }
736
722
  const pr = preds.get(t), sg = sigmaAt.get(t);
737
723
  const bear = isMarketDown(ds, t);
738
- const mktDown = mktFilter && dir !== 'neutral' && bear;
724
+ const mktDown = mktFilter && bear;
739
725
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
740
726
  const need = effMinEdge * 2 * realCost, exitNeed = need * curExitRatio, earlyExitNeed = need * 0.35;
741
727
  let order, entries;
@@ -745,21 +731,11 @@ async function runStabilityLoop(targetId, runRow, st) {
745
731
  const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
746
732
  if (!(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
747
733
  }
748
- const needL = autoAdapt && dir === 'longshort' && bear ? need * 1.25 : need;
749
- const needS = autoAdapt && dir === 'longshort' ? (bear ? need * 0.90 : need * 1.25) : need;
734
+ const needL = autoAdapt && bear ? need * 1.25 : need;
735
+ const needS = autoAdapt ? (bear ? need * 0.90 : need * 1.25) : need;
750
736
  const candL = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]);
751
737
  const candS = [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) < -needS).sort((a, b) => pr[a] - pr[b]);
752
- let nL = K - countSide('long'), nS = K - countSide('short');
753
- if (dir === 'neutral') {
754
- const pairs = Math.min(K, countSide('long') + candL.length, countSide('short') + candS.length);
755
- if (!autoAdapt) {
756
- for (const side of ['long', 'short']) {
757
- const extra = [...held].filter(([, h]) => h.side === side).sort((a, b) => (side === 'long' ? 1 : -1) * (edge(a[0]) - edge(b[0])));
758
- for (const [i, h] of extra.slice(0, Math.max(0, extra.length - pairs))) closePos(i, h, tIn);
759
- }
760
- }
761
- nL = Math.max(0, pairs - countSide('long')); nS = Math.max(0, pairs - countSide('short'));
762
- }
738
+ const nL = K - countSide('long'), nS = K - countSide('short');
763
739
  entries = [...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long', edge(i)]), ...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short', edge(i)])];
764
740
  } else if (effMinEdge > 0) {
765
741
  for (const [i, h] of [...held]) {
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { kvGet, kvSet } from '../db.js';
3
3
 
4
4
  // «Словарь» модели: акции индекса широкого рынка МосБиржи (MOEXBMI, ~100 бумаг)
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { db } from '../db.js';
3
3
  import { hub } from '../hub.js';
4
4
  import { getSettings } from '../settings.js';
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import crypto from 'node:crypto';
3
3
  import https from 'node:https';
4
4
  import fs from 'node:fs';
@@ -1,10 +1,10 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  // Описание всех настроек: для формы в интерфейсе, для ИИ-стратега и для проверки значений.
3
3
  // hidden — не показывать в интерфейсе; ai:false — ИИ менять запрещено (честность проверки).
4
4
  export const PARAMS = [
5
5
  { key: 'modelAutoAdapt', label: 'Самообучение правил торговли (Авто)', hint: 'Полный автомат: стратегия сама учится на мульти-горизонте (1,5–3,5 ч), автоматически управляет порогами входа и выхода под вашу комиссию, масштабирует слоты по силе сигнала и временно ставит на карантин убыточные акции. Ручная настройка горизонта и порогов не требуется.', type: 'checkbox' },
6
6
  { key: 'modelTopK', label: 'Сколько акций держать', hint: 'Сколько акций с лучшим прогнозом можно держать одновременно (при продажах — и столько же акций на продажу).', min: 1, max: 20 },
7
- { key: 'modelDirection', label: 'Направление торговли', hint: '«Только в рост (без шорта)» — классическая покупка акций и их продажа при фиксации прибыли (работает на любом счёте и ИИС). «В рост и в падение (с шортом)» — дополнительно зарабатывает на падающих акциях за счёт коротких продаж (требуется включённая маржинальная торговля у брокера; на брокерских счетах без маржиналки шорты отключаются автоматически). «Хедж 50/50 (рост + падение)» — держит позиции в рост и в падение на равные суммы, чтобы не зависеть от обвалов индекса.', type: 'select', options: [['long', 'Только в рост (без шорта)'], ['longshort', 'В рост и в падение (с шортом)'], ['neutral', 'Хедж 50/50 (рост + падение)']] },
7
+ { key: 'modelDirection', label: 'Направление торговли', hint: '«Только в рост (без шорта)» — классическая покупка акций и их продажа при фиксации прибыли (работает на любом счёте и ИИС). «В рост и в падение (с шортом)» — дополнительно зарабатывает на падающих акциях за счёт коротких продаж (требуется включённая маржинальная торговля у брокера; на брокерских счетах без маржиналки шорты отключаются автоматически).', type: 'select', options: [['long', 'Только в рост (без шорта)'], ['longshort', 'В рост и в падение (с шортом)']] },
8
8
  { key: 'modelHorizon', label: 'Горизонт прогноза, часов (ручной режим)', hint: 'При включённом Авто-режиме управляется автоматически (мульти-горизонт 1,5–3,5 ч). При выключенном Авто — на сколько часов вперёд предсказывать движение.', min: 0.5, max: 8 },
9
9
  { key: 'modelMinEdge', label: 'Порог выгоды на вход (ручной режим)', hint: 'При включённом Авто-режиме управляется автоматически (2,5× издержек). При выключенном Авто — во сколько раз ожидаемое движение должно превышать стоимость сделки.', min: 0, max: 10 },
10
10
  { key: 'modelExitRatio', label: 'Порог на выход (ручной режим)', hint: 'При включённом Авто-режиме управляется автоматически (двухступенчатый выход 0,35 / 0,95). При выключенном Авто — до какой доли от порога входа держать позицию.', min: 0, max: 1, step: 0.05 },
@@ -37,7 +37,8 @@ export function validateParam(key, raw) {
37
37
  if (!m) return { error: `нет такой настройки «${key}»` };
38
38
  if (m.type === 'checkbox') return { value: raw === true || raw === 'true' || raw === 1 };
39
39
  if (m.type === 'select') {
40
- const opt = m.options.find(([v]) => String(v) === String(raw));
40
+ const normRaw = key === 'modelDirection' && String(raw) === 'neutral' ? 'longshort' : raw;
41
+ const opt = m.options.find(([v]) => String(v) === String(normRaw));
41
42
  return opt ? { value: opt[0] } : { error: `${key}: допустимо ${m.options.map(([v]) => v).join(', ')}` };
42
43
  }
43
44
  // число; запятая допустима как десятичный разделитель («2,5» = 2.5)
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { strategyDefaults } from './config.js';
3
3
  import { userKvGet, userKvSet, addSettingsHistory, getSettingsHistory } from './db.js';
4
4
  import { validateParam, normalizeKey, PARAM_MAP } from './params-meta.js';
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { strategyDefaults } from '../config.js';
3
3
  import { getRun, getTrades, listRuns } from '../db.js';
4
4
  import { round, MSK } from '../util.js';
@@ -45,7 +45,7 @@ export function runSummary(run, { revealOOS = false } = {}) {
45
45
  s.горизонт_часов = m.model.horizon;
46
46
  s.акций = m.model.tokens;
47
47
  const dir = run.params?.modelDirection || 'long';
48
- if (dir !== 'long') s.направление = dir === 'neutral' ? 'нейтрально к рынку (покупки и продажи парами; сравнивать с нулём, а не с рынком)' : 'покупки и продажи';
48
+ if (dir !== 'long') s.направление = 'в рост и в падение (с шортом)';
49
49
  s.период_настройки = Object.fromEntries(Object.entries(cmp).map(([n, c]) => [LBL[n] || n, seg(c, 'inSample', dir)]));
50
50
  s.контрольный_период = revealOOS
51
51
  ? Object.fromEntries(Object.entries(cmp).map(([n, c]) => [LBL[n] || n, seg(c, 'outOfSample', dir)]))
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { PARAMS } from '../params-meta.js';
3
3
  import { chatStream, activeModelInfo, isLlmEnabled } from '../llm/client.js';
4
4
  import { hub } from '../hub.js';
@@ -86,7 +86,7 @@ const PARAMS_VISIBLE = PARAMS.filter((p) => !p.hidden && !p.fixed);
86
86
  доход портфеля из лучших прогнозов с комиссиями и БЕЗ комиссий (разница показывает, сколько съедают издержки), сравнение с рынком.
87
87
  Сделки исполняются по цене открытия следующей свечи (честно). Порог выгоды modelMinEdge: покупаем, только если ожидаемое движение больше стоимости сделки × порог.
88
88
  У стратегии включено самообучение правил торговли (modelAutoAdapt=true по умолчанию — Полный Автомат): мульти-горизонтное обучение (1.5–3.5 ч), сопровождение на полушаге H/2 (мягкий порог 0.35 на первых двух полушагах и 0.95 по зрелости), взвешивание слота по силе сигнала (Conviction Sizing), адаптация порогов покупок и продаж к среднесрочному тренду индекса Мосбиржи и временный карантин убыточных «акций-убийц» с затуханием. Дополнительно есть настройки: число акций (modelTopK), направление (modelDirection), способ распределения долей (modelAllocMode: equal / invvol), фильтр падающего рынка (modelMarketFilter: off / ma), а при выключенном modelAutoAdapt — ручные горизонт (modelHorizon), порог входа (modelMinEdge) и порог выхода (modelExitRatio).
89
- Направление modelDirection: long — только покупки (по умолчанию); longshort — ещё и продажи без покупки (шорт) акций, ожидаемо слабее рынка (ожидание ниже −порога); neutral — парами: равные суммы покупок и продаж, нет пары — не торгуем; общий рост или падение рынка почти не влияют, результат сравнивай с нулём, а не с рынком. С продажами на позицию приходится капитал/2К. За продажу брокер берёт плату за заём акций modelShortFeePct (% годовых за время удержания). В разборах есть вклад покупок и продаж и разбивка по стороне — смотри, какая сторона зарабатывает.
89
+ Направление modelDirection: long — только в рост (без шорта); longshort — в рост и в падение (с шортом, по умолчанию): покупает лидеров и шортит акции, ожидаемо слабее рынка (ожидание ниже −порога), с авто-балансировкой по тренду индекса. За продажу без покупки брокер берёт плату за заём акций modelShortFeePct (% годовых за время удержания). В разборах есть вклад покупок и продаж и разбивка по стороне — смотри, какая сторона зарабатывает.
90
90
  Разница в итоге меньше 5 п.п. между двумя исследованиями обычно в пределах случайности — не называй её надёжной без контрольного периода.
91
91
  model_forecast — прогноз сейчас, market_neighbors — соседи акции на карте.
92
92
  get_news — свежие новости и раскрытия эмитентов; новости существуют только «сейчас» (в проверках на истории их нет — там они не используются).
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { PARAMS, PARAM_MAP } from '../params-meta.js';
3
3
  import { getSettings, updateSettings, validateChanges } from '../settings.js';
4
4
  import { getRun, listRuns, addJournal, listJournal, kvGet, kvSet } from '../db.js';
package/server/util.js CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  export const DAY = 86400000;
3
3
  export const MSK = 3 * 3600000;
4
4
  export const round = (v, d = 2) => (Number.isFinite(v) ? Math.round(v * 10 ** d) / 10 ** d : v);