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.
- package/LICENSE +13 -17
- package/README.md +4 -2
- package/bin/cli.js +1 -1
- package/deploy/install.sh +1 -1
- 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 +1 -1
- package/package.json +2 -2
- package/public/admin.html +43 -20
- package/public/app.js +780 -226
- package/public/index.html +43 -3
- package/public/landing.html +328 -115
- package/public/legal.html +37 -16
- package/public/styles.css +24 -3
- package/server/api/routes.js +62 -30
- package/server/auth/index.js +109 -20
- package/server/billing/yookassa.js +1 -1
- package/server/brokers/index.js +256 -53
- package/server/config.js +1 -1
- package/server/data/moex.js +1 -1
- package/server/db.js +17 -3
- package/server/hub.js +1 -1
- package/server/index.js +4 -4
- package/server/llm/client.js +1 -1
- package/server/llm/providers.js +1 -1
- package/server/model/dataset.js +1 -1
- package/server/model/forecast.js +354 -25
- package/server/model/linalg.js +1 -1
- package/server/model/live.js +36 -25
- package/server/model/metrics.js +1 -1
- package/server/model/models.js +1 -1
- package/server/model/profiles.js +1 -1
- package/server/model/research.js +14 -38
- package/server/model/vocab.js +1 -1
- package/server/news/index.js +1 -1
- package/server/notify/index.js +1 -1
- package/server/params-meta.js +4 -3
- package/server/settings.js +1 -1
- package/server/strategist/analytics.js +2 -2
- package/server/strategist/index.js +2 -2
- package/server/strategist/tools.js +1 -1
- package/server/util.js +1 -1
package/server/model/research.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
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(
|
|
316
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.
|
|
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 &&
|
|
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 &&
|
|
363
|
-
const needS = autoAdapt
|
|
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
|
-
|
|
367
|
-
|
|
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.
|
|
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)) :
|
|
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 &&
|
|
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 &&
|
|
749
|
-
const needS = autoAdapt
|
|
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
|
-
|
|
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]) {
|
package/server/model/vocab.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
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 бумаг)
|
package/server/news/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
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';
|
package/server/notify/index.js
CHANGED
package/server/params-meta.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
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: '«Только в рост (без шорта)» — классическая покупка акций и их продажа при фиксации прибыли (работает на любом счёте и ИИС). «В рост и в падение (с шортом)» — дополнительно зарабатывает на падающих акциях за счёт коротких продаж (требуется включённая маржинальная торговля у брокера; на брокерских счетах без маржиналки шорты отключаются автоматически).
|
|
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
|
|
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)
|
package/server/settings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin.
|
|
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.
|
|
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.направление =
|
|
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.
|
|
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 — только
|
|
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.
|
|
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.
|
|
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);
|