neuroalpha 0.3.12 → 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 (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 +524 -211
  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 +61 -29
  15. package/server/auth/index.js +109 -20
  16. package/server/billing/yookassa.js +1 -1
  17. package/server/brokers/index.js +179 -34
  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 +188 -17
  27. package/server/model/linalg.js +1 -1
  28. package/server/model/live.js +6 -19
  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 +12 -36
  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,13 +1,13 @@
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 { round, DAY, fmtBarMsk } from '../util.js';
4
- import { loadDataset, barsFromHours, BAR_MS, BAR_INTERVAL } from './dataset.js';
5
- import { buildFold, MODELS, HEADS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
4
+ import { loadDataset, barsFromHours, BAR_MS } from './dataset.js';
5
+ import { buildFold, MODELS, HEADS, SCALES, BETAS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
6
6
  import { loadProfiles } from './profiles.js';
7
7
  import { isMarketDown } from './research.js';
8
8
  import { newsForSymbols } from '../news/index.js';
9
9
 
10
- let cache = null;
10
+ let baseState = null; // { key, trainedAt, ds, last, r, snap, prevExpMap, prevRankMap, lastTickResult, lastTickAt }
11
11
  const inflight = new Map();
12
12
 
13
13
  export const lastClosedIndex = (ds, now = Date.now()) => {
@@ -16,9 +16,100 @@ export const lastClosedIndex = (ds, now = Date.now()) => {
16
16
  return last;
17
17
  };
18
18
 
19
+ // Быстрое получение живых цен Мосбиржи (TQBR + IMOEX) за 1 лёгкий запрос (~80 мс)
20
+ async function fetchLiveMoexSnapshot() {
21
+ try {
22
+ const [rShares, rIdx] = await Promise.all([
23
+ fetch('https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQBR/securities.json?iss.meta=off&iss.only=marketdata&marketdata.columns=SECID,LAST,MARKETPRICE,LCURRENTPRICE,VALTODAY', { signal: AbortSignal.timeout(6000) }),
24
+ fetch('https://iss.moex.com/iss/engines/stock/markets/index/boards/SNDX/securities/IMOEX.json?iss.meta=off&iss.only=marketdata&marketdata.columns=SECID,CURRENTVALUE,LASTVALUE', { signal: AbortSignal.timeout(6000) }).catch(() => null),
25
+ ]);
26
+ if (!rShares.ok) return null;
27
+ const j = await rShares.json();
28
+ const cols = j.marketdata?.columns || [];
29
+ const iSec = cols.indexOf('SECID');
30
+ const iLast = cols.indexOf('LAST');
31
+ const iMkt = cols.indexOf('MARKETPRICE');
32
+ const iCur = cols.indexOf('LCURRENTPRICE');
33
+ const prices = new Map();
34
+ for (const row of j.marketdata?.data || []) {
35
+ const sym = row[iSec];
36
+ const px = Number(row[iLast] || row[iMkt] || row[iCur] || 0);
37
+ if (sym && px > 0) prices.set(sym, px);
38
+ }
39
+ let imoexPrice = 0;
40
+ if (rIdx?.ok) {
41
+ const ji = await rIdx.json().catch(() => null);
42
+ const ic = ji?.marketdata?.columns || [];
43
+ const ir = ji?.marketdata?.data?.[0];
44
+ if (ir) imoexPrice = Number(ir[ic.indexOf('CURRENTVALUE')] || ir[ic.indexOf('LASTVALUE')] || 0);
45
+ }
46
+ return { prices, imoexPrice, ts: Date.now() };
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // Мгновенное обновление последней точки контекста u в уже обученной модели по живым котировкам (за ~2 мс)
53
+ function applyLivePricesToFold(ds, f, u, fitted, liveSnap) {
54
+ if (!liveSnap?.prices?.size) return;
55
+ const idx = f.lo + u;
56
+ if (idx < 1 || idx >= ds.T) return;
57
+ const N = f.N;
58
+ const mPrev = ds.mclose?.[idx - 1] || 0;
59
+ const mLive = liveSnap.imoexPrice > 0 && mPrev > 0 ? Math.log(liveSnap.imoexPrice / mPrev) : ds.mret[idx];
60
+
61
+ // 1) пересчитываем z[u*N + i] по живой цене акции относительно закрытия предыдущей свечи
62
+ for (let i = 0; i < N; i++) {
63
+ const tk = ds.tokens[i];
64
+ const prevPx = tk.close[idx - 1];
65
+ const livePx = liveSnap.prices.get(tk.symbol);
66
+ if (livePx > 0 && prevPx > 0) {
67
+ const ret = Math.log(livePx / prevPx);
68
+ const e = ret - f.beta[i] * mLive;
69
+ f.z[u * N + i] = Math.max(-8, Math.min(8, e / f.sigma[i]));
70
+ }
71
+ }
72
+
73
+ // 2) пересчитываем признаки масштаба 0 (последний час) в точке u
74
+ const sc0 = SCALES[0];
75
+ const invP = 1 / Math.sqrt(sc0.len), invF = invP / 1.5;
76
+ const Xp0 = f.X[0][0], Xf0 = f.X[1][0];
77
+ for (let i = 0; i < N; i++) {
78
+ let sp = 0, sf = 0;
79
+ for (let k = 0; k < sc0.len; k++) {
80
+ const uk = u - k;
81
+ if (uk < 0) break;
82
+ const zk = f.z[uk * N + i];
83
+ sp += zk;
84
+ sf += zk * Math.min(4, ds.tokens[i].valRatio[f.lo + uk]);
85
+ }
86
+ Xp0[u * N + i] = sp * invP;
87
+ Xf0[u * N + i] = sf * invF;
88
+ }
89
+
90
+ // 3) пересчитываем головы внимания F[hi][u*N + j] в точке u
91
+ const att = fitted.attention;
92
+ if (att?.As && att?.F) {
93
+ const off = u * N;
94
+ for (let hi = 0; hi < HEADS.length; hi++) {
95
+ const h = HEADS[hi];
96
+ const bIdx = BETAS.findIndex((b) => b.v === h.beta);
97
+ const A = att.As[bIdx];
98
+ const X = f.X[h.kind === 'price' ? 0 : 1][h.scale];
99
+ const Fh = att.F[hi];
100
+ for (let j = 0; j < N; j++) {
101
+ let s = 0;
102
+ const row = j * N;
103
+ for (let i = 0; i < N; i++) s += A[row + i] * X[off + i];
104
+ Fh[off + j] = s;
105
+ }
106
+ }
107
+ }
108
+ }
109
+
19
110
  // Обучение на окне, заканчивающемся свечой idx, и прогноз по этой свече.
20
111
  // Возвращает строки по всем акциям: ожидаемое движение (относительно рынка), сигнал покупки, кто влияет.
21
- export async function predictAt(ds, p, idx, { explain = true } = {}) {
112
+ export async function predictAt(ds, p, idx, { explain = true, liveSnap = null } = {}) {
22
113
  const autoAdapt = p.modelAutoAdapt !== false;
23
114
  const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
24
115
  const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
@@ -33,16 +124,23 @@ export async function predictAt(ds, p, idx, { explain = true } = {}) {
33
124
  const chosen = p.modelType in MODELS ? p.modelType : 'attention';
34
125
  const list = [...new Set(explain ? Object.keys(MODELS) : ['attention', chosen])];
35
126
  const fitted = {};
36
- const preds = {};
37
127
  for (const n of list) {
38
128
  fitted[n] = await MODELS[n].fit(f);
129
+ }
130
+ if (liveSnap) applyLivePricesToFold(ds, f, u, fitted, liveSnap);
131
+ return buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap);
132
+ }
133
+
134
+ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap = null) {
135
+ const preds = {};
136
+ for (const n of Object.keys(fitted)) {
39
137
  preds[n] = MODELS[n].predict(fitted[n], f, u);
40
138
  }
41
139
  const cost = (p.commissionPct + p.slippagePct) / 100;
42
140
  const threshold = effMinEdge * 2 * cost;
43
141
  const bear = isMarketDown(ds, idx);
44
142
  const dir = p.modelDirection || 'long';
45
- const needL = autoAdapt && bear && dir !== 'neutral' ? threshold * 1.25 : threshold;
143
+ const needL = autoAdapt && bear ? threshold * 1.25 : threshold;
46
144
  const needS = autoAdapt && dir === 'longshort' ? (bear ? threshold * 0.90 : threshold * 1.25) : threshold;
47
145
  let sInv = 0, nInv = 0;
48
146
  for (let i = 0; i < ds.tokens.length; i++) if (f.sigma[i] > 0 && ds.tokens[i].liq[idx] >= LIQ_MIN) { sInv += 1 / f.sigma[i]; nInv++; }
@@ -50,8 +148,10 @@ export async function predictAt(ds, p, idx, { explain = true } = {}) {
50
148
  const rows = ds.tokens.map((tk, i) => {
51
149
  const exp = preds[chosen][i] * f.sigma[i] * Math.sqrt(H);
52
150
  const weight = avgInv > 0 && f.sigma[i] > 0 ? Math.max(0.4, Math.min(1.8, (1 / f.sigma[i]) / avgInv)) : 1;
151
+ const livePx = liveSnap?.prices?.get(tk.symbol) || tk.close[idx] || 0;
53
152
  const row = {
54
153
  i, symbol: tk.symbol, name: tk.name,
154
+ price: round(livePx, 4),
55
155
  liquid: tk.liq[idx] >= LIQ_MIN,
56
156
  expected: exp,
57
157
  expectedPct: round(exp * 100, 3),
@@ -70,9 +170,7 @@ export async function predictAt(ds, p, idx, { explain = true } = {}) {
70
170
  return { f, u, fitted, chosen, threshold, marketDown: bear, rows };
71
171
  }
72
172
 
73
- // Новости — только в прогнозе «сейчас»: в исторической проверке их нет (истории новостей не существует,
74
- // и ИИ, знающий будущее, исказил бы проверку). Строкам с новостями за последние 24 ч добавляем
75
- // news: { count, tone — средний взвешенный по важности, top — суть самой важной }
173
+ // Новости — только в прогнозе «сейчас»
76
174
  function withNews(result, p) {
77
175
  const nm = p.newsEnabled ? newsForSymbols(24) : null;
78
176
  for (const r of result.rows) {
@@ -83,31 +181,104 @@ function withNews(result, p) {
83
181
  return result;
84
182
  }
85
183
 
86
- // Прогноз на ближайшие H часов по последней закрытой свече
184
+ // Добавляет к каждой строке дельту ожидания (deltaPct) и смещение в рейтинге (rankDelta) относительно предыдущего замера
185
+ function attachDeltas(rows, stateObj) {
186
+ const prevExp = stateObj.prevExpMap;
187
+ const prevRank = stateObj.prevRankMap;
188
+ const nextExp = new Map();
189
+ const nextRank = new Map();
190
+ let changed = false;
191
+
192
+ rows.forEach((r, idx) => {
193
+ const rank = idx + 1;
194
+ nextExp.set(r.symbol, r.expectedPct);
195
+ nextRank.set(r.symbol, rank);
196
+ if (prevExp && prevExp.has(r.symbol)) {
197
+ const d = round(r.expectedPct - prevExp.get(r.symbol), 3);
198
+ if (Math.abs(d) >= 0.001) changed = true;
199
+ r.deltaPct = d !== 0 ? d : (r.deltaPct || 0);
200
+ r.rankDelta = prevRank?.has(r.symbol) ? (prevRank.get(r.symbol) - rank) : 0;
201
+ } else {
202
+ r.deltaPct = 0;
203
+ r.rankDelta = 0;
204
+ }
205
+ });
206
+
207
+ if (!prevExp || changed) {
208
+ stateObj.prevExpMap = nextExp;
209
+ stateObj.prevRankMap = nextRank;
210
+ }
211
+ }
212
+
213
+ // Прогноз на ближайшие H часов с живым поминутным обновлением котировок (без переобучения тяжёлого окна каждую минуту)
87
214
  export async function forecastNow(input, { force = false, onProgress } = {}) {
88
215
  const p = normalizeParams(input);
89
216
  const autoAdapt = p.modelAutoAdapt !== false;
90
217
  const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
91
218
  const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
92
219
  const key = JSON.stringify([autoAdapt, p.modelType, effHorizon, p.modelTrainDays, p.modelEmbedDim, effMinEdge, p.modelDirection, p.modelUseMacro, p.modelUseDividends, p.modelTextWeight, p.commissionPct, p.slippagePct]);
93
- if (!force && cache && cache.key === key && Date.now() - cache.at < 10 * 60000) return withNews(cache.result, p);
220
+
221
+ const now = Date.now();
222
+ // 1) Если с последнего живого тика прошло меньше 15 секунд и нет force — отдаём мгновенно из памяти
223
+ if (!force && baseState && baseState.key === key && baseState.lastTickResult && now - baseState.lastTickAt < 15000) {
224
+ return withNews(baseState.lastTickResult, p);
225
+ }
226
+ // 2) Если базовая модель обучена менее 10 минут назад и нет force — делаем быстрый живой пересчёт (~80 мс запрос + 2 мс математика)
227
+ if (!force && baseState && baseState.key === key && now - baseState.trainedAt < 10 * 60000) {
228
+ const liveSnap = await fetchLiveMoexSnapshot();
229
+ if (liveSnap) applyLivePricesToFold(baseState.ds, baseState.f, baseState.u, baseState.fitted, liveSnap);
230
+ const H = barsFromHours(effHorizon);
231
+ const r = buildPredictionRows(baseState.ds, p, baseState.last, baseState.f, baseState.u, baseState.fitted, baseState.chosen, H, effMinEdge, autoAdapt, true, liveSnap);
232
+ const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: baseState.snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
233
+ attachDeltas(sortedRows, baseState);
234
+ const result = {
235
+ ...baseState.lastTickResult,
236
+ at: now,
237
+ liveTs: liveSnap?.ts || now,
238
+ rows: sortedRows,
239
+ };
240
+ baseState.lastTickResult = result;
241
+ baseState.lastTickAt = now;
242
+ return withNews(result, p);
243
+ }
244
+
94
245
  if (!force && inflight.has(key)) return withNews(await inflight.get(key), p);
95
246
  const task = (async () => {
96
- const now = Date.now();
97
- const ds = await loadDataset({ fromTs: now - Math.ceil(p.modelTrainDays * 1.5 + 25) * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, onProgress });
247
+ const [ds, liveSnap] = await Promise.all([
248
+ loadDataset({ fromTs: now - Math.ceil(p.modelTrainDays * 1.5 + 25) * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, onProgress }),
249
+ fetchLiveMoexSnapshot(),
250
+ ]);
98
251
  const last = lastClosedIndex(ds, now);
99
- const r = await predictAt(ds, p, last);
252
+ const r = await predictAt(ds, p, last, { explain: true, liveSnap });
100
253
  const snap = mapSnapshot(r.f);
254
+ const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
255
+ const stateObj = baseState && baseState.key === key ? baseState : { prevExpMap: null, prevRankMap: null };
256
+ attachDeltas(sortedRows, stateObj);
101
257
  const result = {
102
258
  at: now,
259
+ liveTs: liveSnap?.ts || now,
103
260
  barTs: ds.timeline[last],
104
261
  bar: fmtBarMsk(ds.timeline[last]),
105
262
  horizon: effHorizon, direction: p.modelDirection, model: r.chosen, thresholdPct: round(r.threshold * 100, 3), marketDown: r.marketDown,
106
- rows: r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct),
263
+ rows: sortedRows,
107
264
  map: { symbols: ds.tokens.map((t) => t.symbol), names: ds.tokens.map((t) => t.name), pts: snap.pts.map(([x, y]) => [round(x, 3), round(y, 3)]), cl: snap.clusters },
108
265
  heads: HEADS.map((h, i) => ({ label: h.label, coef: round(r.fitted.attention.coef[i], 4) })),
109
266
  };
110
- cache = { key, at: now, result };
267
+ baseState = {
268
+ key,
269
+ trainedAt: now,
270
+ ds,
271
+ last,
272
+ f: r.f,
273
+ u: r.u,
274
+ fitted: r.fitted,
275
+ chosen: r.chosen,
276
+ snap,
277
+ prevExpMap: stateObj.prevExpMap,
278
+ prevRankMap: stateObj.prevRankMap,
279
+ lastTickResult: result,
280
+ lastTickAt: now,
281
+ };
111
282
  return result;
112
283
  })().finally(() => inflight.delete(key));
113
284
  inflight.set(key, task);
@@ -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
  // Небольшая линейная алгебра без зависимостей: матрицы — массивы Float64Array (строки)
3
3
 
4
4
  export const matrix = (n, m) => Array.from({ length: n }, () => new Float64Array(m));
@@ -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 { createRun, finishRun, saveTrade, updateRunMetrics, getRun, getTrades, deleteRun, kvGet, kvSet } from '../db.js';
3
3
  import { normalizeParams } from '../settings.js';
4
4
  import { round, DAY, MSK, fmtBarMsk } from '../util.js';
@@ -308,7 +308,7 @@ class LiveEngine {
308
308
  const dir = this.#direction(s);
309
309
  const slots = dir === 'long' ? K : 2 * K;
310
310
  const autoAdapt = s.params.modelAutoAdapt !== false;
311
- 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)));
311
+ const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : Math.min(0.35, 1.4 / (2 * K));
312
312
  const invVol = s.params.modelAllocMode === 'invvol';
313
313
  const effMinEdge = autoAdapt ? 2.5 : s.params.modelMinEdge;
314
314
  const needPct = effMinEdge * 2 * (s.params.commissionPct + s.params.slippagePct);
@@ -373,7 +373,7 @@ class LiveEngine {
373
373
  closing.add(sym);
374
374
  };
375
375
  const exitRatio = autoAdapt ? 0.95 : Number(p.modelExitRatio ?? 0.5);
376
- const mktDown = p.modelMarketFilter === 'ma' && dir !== 'neutral' && r.marketDown;
376
+ const mktDown = p.modelMarketFilter === 'ma' && r.marketDown;
377
377
  const quarantined = new Set();
378
378
  if (autoAdapt && s.trades.length >= 3) {
379
379
  const stMap = new Map();
@@ -418,22 +418,9 @@ class LiveEngine {
418
418
  const open = (x) => !s.held.has(x.symbol) || closing.has(x.symbol);
419
419
  const candL = mktDown ? [] : r.rows.filter((x) => x.signal && open(x)).sort((a, b) => b.expected - a.expected);
420
420
  const candS = r.rows.filter((x) => x.shortSignal && open(x)).sort((a, b) => a.expected - b.expected);
421
- if (mktDown && dir === 'longshort') step.decisions.push({ action: 'skip', symbol: 'IMOEX', text: 'Новые покупки пропущены: индекс Мосбиржи падает' });
422
- let nL, nS;
423
- if (dir === 'longshort') {
424
- nL = K - sideHeld('long').length;
425
- nS = K - sideHeld('short').length;
426
- } else {
427
- const pairs = Math.min(K, sideHeld('long').length + candL.length, countSideShort(sideHeld('short').length, candS.length));
428
- if (!autoAdapt) {
429
- for (const side of ['long', 'short']) {
430
- const extra = sideHeld(side).sort((a, b) => (side === 'long' ? 1 : -1) * ((row.get(a[0])?.expected ?? 0) - (row.get(b[0])?.expected ?? 0)));
431
- for (const [sym] of extra.slice(0, Math.max(0, extra.length - pairs))) closePos(sym, 'Нет пары с другой стороны');
432
- }
433
- }
434
- nL = Math.max(0, pairs - sideHeld('long').length);
435
- nS = Math.max(0, pairs - sideHeld('short').length);
436
- }
421
+ if (mktDown) step.decisions.push({ action: 'skip', symbol: 'IMOEX', text: 'Новые покупки пропущены: индекс Мосбиржи падает' });
422
+ const nL = K - sideHeld('long').length;
423
+ const nS = K - sideHeld('short').length;
437
424
  entries = [...take(candL, 'buy', nL).map((x) => [x, 'buy']), ...take(candS, 'short', nS).map((x) => [x, 'short'])];
438
425
  }
439
426
  for (const [x, action] of entries) {
@@ -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 { round } from '../util.js';
3
3
 
4
4
  const MSK = 3 * 3600000;
@@ -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 { eigenSym, ridgeSolve, matrix, spearman, kmeans } from './linalg.js';
3
3
  import { BAR_INTERVAL } from './dataset.js';
4
4
 
@@ -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
  import { ensureVocab } from './vocab.js';
4
4
  import { chatStream, extractJson, isLlmEnabled } from '../llm/client.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 { normalizeParams } from '../settings.js';
3
3
  import { computeReport, downsample, buildPeriods } from './metrics.js';
4
4
  import { round } from '../util.js';
@@ -313,7 +313,7 @@ async function run(runId, p, state) {
313
313
  const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
314
314
  let curExitRatio = autoAdapt ? 0.95 : exitRatio;
315
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)));
316
+ const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : Math.min(0.35, 1.4 / (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)]),
@@ -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);