neuroalpha 0.3.15 → 0.3.17

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.
@@ -34,8 +34,8 @@ export function isMarketDown(ds, t) {
34
34
  }
35
35
 
36
36
  // Версия обработки данных: при её смене прежние прогоны не считаются «такими же» (у старых её нет).
37
- // 7 — мульти-горизонтное обучение (0.6H / 1.0H / 1.4H) + взвешивание слота по силе сигнала (Conviction Sizing) в Авто-режиме
38
- const DATA_VERSION = 7;
37
+ // 8 — скорректированный спектр Марченко–Пастура (sigmaBulk) для 0 — Авто + динамическая концентрация слота (Conviction Sizing)
38
+ const DATA_VERSION = 8;
39
39
 
40
40
  const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
41
41
  function reuseParamsKey(params) {
@@ -147,8 +147,13 @@ async function run(runId, p, state) {
147
147
  const H = barsFromHours(effHorizon); // горизонт в свечах по 10 минут
148
148
  const maxH = autoAdapt ? Math.round(H * 1.4) : H;
149
149
  const tillTs = Date.now();
150
- const researchFrom = tillTs - p.modelHistoryDays * DAY;
151
- const fromTs = researchFrom - Math.ceil(p.modelTrainDays * 1.5 + 20) * DAY;
150
+ // Выравниваем начало периода исследования по началу календарных суток МСК,
151
+ // а для чтения датасета из SQLite всегда берём единый широкий горизонт (fromTs = 0),
152
+ // чтобы смена «Окна обучения» (modelTrainDays) или «Периода исследования» (modelHistoryDays)
153
+ // НИКОГДА не сдвигала fromTs и не вызывала повторную загрузку котировок!
154
+ const todayStartMsk = Math.floor((tillTs + MSK) / DAY) * DAY - MSK;
155
+ const researchFrom = todayStartMsk - p.modelHistoryDays * DAY;
156
+ const fromTs = todayStartMsk - 1650 * DAY;
152
157
 
153
158
  emit({ stage: 'data', pct: 0, text: 'Загружаем словарь рынка (состав индекса широкого рынка)…' });
154
159
  const ds = await loadDataset({
@@ -250,18 +255,39 @@ async function run(runId, p, state) {
250
255
  self: [round(attM.coef[HEADS.length], 4), round(attM.coef[HEADS.length + 1], 4), round(attM.coef[HEADS.length + 2], 4)],
251
256
  } : {}),
252
257
  };
253
- emit({ stage: 'sim', pct: Math.round(((fi + 1) / folds) * 100), text: mainText, viz: liveViz });
258
+ emit({ stage: 'sim', pct: Math.round(((fi + 1) / folds) * 80), text: mainText, viz: liveViz });
254
259
  testStart = testEnd;
255
260
  fi++;
256
261
  }
257
262
 
258
- emit({ stage: 'sim', pct: 100, text: 'Считаем результаты…' });
263
+ emit({ stage: 'calc', pct: 81, text: 'Финальный пересчёт · подготовка временного ряда прогнозов…' });
259
264
  await tick();
260
265
  const testTs = [...yraw.keys()].sort((a, b) => a - b);
261
266
  const tsOf = (t) => timeline[t];
262
267
 
263
268
  // ---------- портфель из лучших прогнозов: покупки, продажи без покупки, пары «нейтрально к рынку» ----------
264
- const K = p.modelTopK;
269
+ const rawTopK = Number(p.modelTopK ?? 2);
270
+ const autoK = rawTopK === 0 || autoAdapt;
271
+ const K = rawTopK > 0 ? rawTopK : 4;
272
+ // Адаптивный отбор числа акций (Dynamic Top-K):
273
+ // лидер №1 берётся всегда при пробое порога; следующие бумаги (2…K) берутся только если их сигнал
274
+ // достаточно силён относительно лидера (>= 68% от силы №1) и уверенно перекрывает порог (>= 1.25 * need),
275
+ // чтобы не размывать капитал на слабые окраинные сигналы
276
+ const filterDynamicTopK = (cands, edgeFn, needVal, maxCount) => {
277
+ if (!cands.length || maxCount <= 0) return [];
278
+ const lim = Math.min(cands.length, maxCount);
279
+ if (!autoK || lim <= 1) return cands.slice(0, lim);
280
+ const bestE = Math.abs(edgeFn(cands[0]));
281
+ const out = [cands[0]];
282
+ for (let m = 1; m < lim; m++) {
283
+ const curE = Math.abs(edgeFn(cands[m]));
284
+ const relMin = m === 1 ? 0.62 : 0.78;
285
+ const absMin = m === 1 ? needVal * 1.15 : needVal * 1.45;
286
+ if (curE >= bestE * relMin && curE >= absMin) out.push(cands[m]);
287
+ else break;
288
+ }
289
+ return out;
290
+ };
265
291
  const dir = p.modelDirection;
266
292
  const exitRatio = Number(p.modelExitRatio ?? 0.5);
267
293
  const mktFilter = p.modelMarketFilter === 'ma';
@@ -313,7 +339,7 @@ async function run(runId, p, state) {
313
339
  const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
314
340
  let curExitRatio = autoAdapt ? 0.95 : exitRatio;
315
341
  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));
342
+ const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
317
343
 
318
344
  const closePos = (i, h, t, end, why) => {
319
345
  const exitPrice = px(i, t);
@@ -365,9 +391,11 @@ async function run(runId, p, state) {
365
391
  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
392
  const nL = K - countSide('long');
367
393
  const nS = K - countSide('short');
394
+ const pickedL = filterDynamicTopK(candL, edge, needL, Math.max(0, nL));
395
+ const pickedS = filterDynamicTopK(candS, edge, needS, Math.max(0, nS));
368
396
  entries = [
369
- ...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long', edge(i)]),
370
- ...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short', edge(i)]),
397
+ ...pickedL.map((i) => [i, 'long', edge(i)]),
398
+ ...pickedS.map((i) => [i, 'short', edge(i)]),
371
399
  ];
372
400
  } else if (effMinEdge > 0) {
373
401
  // ожидаемое движение (в долях) = прогноз × обычный размах × √H; входим, только если оно окупает сделку
@@ -383,7 +411,8 @@ async function run(runId, p, state) {
383
411
  }
384
412
  const free = K - held.size;
385
413
  const needL = autoAdapt && bear ? need * 1.25 : need;
386
- const cand = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]).slice(0, Math.max(0, free));
414
+ const rawCand = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]);
415
+ const cand = filterDynamicTopK(rawCand, edge, needL, Math.max(0, free));
387
416
  order = [...held.keys(), ...cand];
388
417
  } else {
389
418
  const cand = mktDown ? [] : [...pr.keys()].filter((i) => stockAllowed(i) && pr[i] > 0 && liquid(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, K);
@@ -402,11 +431,29 @@ async function run(runId, p, state) {
402
431
  const usedStart = [...held.values()].reduce((s, h) => s + h.alloc, 0);
403
432
  let avail = Math.max(0, equity * 0.99 - usedStart);
404
433
  const needEdge = effMinEdge * 2 * realCost;
434
+ // Динамическая концентрация капитала под реально работающие позиции:
435
+ // если при K=5 на рынке есть всего 1–2 сильных сигнала, не замораживаем 60% денег в кэше под пустые слоты,
436
+ // а взвешиваем слот от фактического числа занятых + новых позиций и силы каждого сигнала (Conviction Sizing)
437
+ const activeTotal = Math.max(2, held.size + newEntries.length);
438
+ const dynSlotWeight = autoAdapt
439
+ ? Math.min(dir === 'long' ? 0.58 : 0.75, Math.max(maxSlotWeight, (dir === 'long' ? 1.8 : 3.0) / Math.min(slots, activeTotal + 1)))
440
+ : maxSlotWeight;
441
+ // Сортируем новые входы по убыванию силы сигнала, чтобы лучшие идеи получали приоритетную долю капитала
442
+ if (autoAdapt && newEntries.length > 1) {
443
+ newEntries.sort((a, b) => Math.abs(b[2] ?? 0) - Math.abs(a[2] ?? 0));
444
+ }
445
+ const edgeWeights = newEntries.map(([i, , edgeAt]) => {
446
+ const effE = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
447
+ return Math.max(0.0001, effE);
448
+ });
449
+ let remWeightSum = edgeWeights.reduce((a, b) => a + b, 0);
405
450
  for (let idx = 0; idx < newEntries.length; idx++) {
406
451
  const [i, side, edgeAt] = newEntries[idx];
407
- const effEdge = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
408
- const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.25, Math.max(0.85, 0.80 + 0.25 * (effEdge / needEdge))) : 1;
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));
452
+ const effEdge = edgeWeights[idx];
453
+ const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.65, Math.max(0.85, 0.80 + 0.30 * (effEdge / needEdge))) : 1;
454
+ const propShare = autoAdapt && remWeightSum > 0 ? avail * (effEdge / remWeightSum) : avail / Math.max(1, newEntries.length - idx);
455
+ remWeightSum = Math.max(0, remWeightSum - effEdge);
456
+ const baseAlloc = autoAdapt ? Math.min(equity * dynSlotWeight * conv, propShare, avail) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
410
457
  let alloc = baseAlloc;
411
458
  if (invVol && avgInv > 0 && sg[i] > 0) {
412
459
  const w = Math.max(0.4, Math.min(1.8, (1 / sg[i]) / avgInv));
@@ -465,9 +512,15 @@ async function run(runId, p, state) {
465
512
  const cost = realCost;
466
513
  const results = {};
467
514
  let splitTs = null;
468
- for (const n of names) {
515
+ for (let ni = 0; ni < names.length; ni++) {
516
+ const n = names[ni];
517
+ const lbl = MODELS[n]?.label || n;
518
+ emit({ stage: 'calc', pct: 82 + ni * 2, text: `Финальный пересчёт · симуляция портфеля (${ni + 1} из ${names.length}): «${lbl}»…` });
519
+ await tick();
520
+ if (state.cancelled) throw new Error('Отменено');
469
521
  const sim = simulate(n, cost);
470
522
  const report = computeReport(sim.trades, sim.curve, p.capital, p.oosPct);
523
+ await tick();
471
524
  const gross = simulate(n, 0, 0);
472
525
  const grossReport = computeReport(gross.trades, gross.curve, p.capital, p.oosPct);
473
526
  splitTs = report.splitTs;
@@ -478,11 +531,16 @@ async function run(runId, p, state) {
478
531
  return seg.length > 1 ? (seg[seg.length - 1].equity / seg[0].equity - 1) * 100 : 0;
479
532
  };
480
533
  const SEG = { full: [-Infinity, Infinity], inSample: [-Infinity, splitTs - 1], outOfSample: [splitTs, Infinity] };
481
- const icStats = (n) => {
534
+ const icStats = async (n) => {
482
535
  const out = {};
483
536
  const weekly = new Map();
484
537
  const all = [];
485
- for (const t of testTs) {
538
+ for (let idx = 0; idx < testTs.length; idx++) {
539
+ if ((idx & 2047) === 0) {
540
+ await tick();
541
+ if (state.cancelled) throw new Error('Отменено');
542
+ }
543
+ const t = testTs[idx];
486
544
  // совпадение прогноза с фактом — только по ликвидным акциям
487
545
  const pr = preds[n].get(t), y = yraw.get(t);
488
546
  const ok = [...pr.keys()].filter((i) => tokens[i].liq[t] >= LIQ_MIN);
@@ -508,8 +566,12 @@ async function run(runId, p, state) {
508
566
 
509
567
  const comparison = {};
510
568
  const weeklyIc = {};
511
- for (const n of names) {
512
- const ic = icStats(n);
569
+ for (let ni = 0; ni < names.length; ni++) {
570
+ const n = names[ni];
571
+ const lbl = MODELS[n]?.label || n;
572
+ emit({ stage: 'calc', pct: 88 + ni * 2, text: `Финальный пересчёт · ранговая точность IC по неделям (${ni + 1} из ${names.length}): «${lbl}»…` });
573
+ await tick();
574
+ const ic = await icStats(n);
513
575
  weeklyIc[n] = ic.weekly;
514
576
  const rep = results[n].report;
515
577
  comparison[n] = {};
@@ -529,7 +591,8 @@ async function run(runId, p, state) {
529
591
  }
530
592
 
531
593
  const chosen = names.includes(p.modelType) ? p.modelType : 'attention';
532
- for (const t of results[chosen].sim.trades) saveTrade(runId, t);
594
+ emit({ stage: 'calc', pct: 95, text: `Финальный пересчёт · проверка фазовой устойчивости (5 сдвигов точки входа)…` });
595
+ await tick();
533
596
  const report = results[chosen].report;
534
597
  report.period = { from: tsOf(testTs[0]), till: tsOf(testTs[testTs.length - 1]) };
535
598
  report.bars = testTs.length;
@@ -546,18 +609,29 @@ async function run(runId, p, state) {
546
609
  // Быстрая проверка устойчивости по сдвигам точки пересмотра и старта (на готовых прогнозах, без переобучения)
547
610
  const bpdInt = Math.max(1, Math.round(bpd));
548
611
  const offsets = [...new Set([0, Math.max(1, Math.floor(H / 3)), Math.max(2, Math.floor((2 * H) / 3)), bpdInt * 2, bpdInt * 4].filter((o) => o < Math.min(testTs.length / 4, bpdInt * 10)))];
549
- const stabRuns = offsets.map((off, idx) => {
612
+ const stabRuns = [];
613
+ for (let idx = 0; idx < offsets.length; idx++) {
614
+ const off = offsets[idx];
615
+ if (idx > 0) await tick();
550
616
  const sm = idx === 0 ? results[chosen].sim : simulate(chosen, cost, p.modelShortFeePct, off);
551
617
  const rep = idx === 0 ? results[chosen].report.full : computeReport(sm.trades, sm.curve, p.capital, p.oosPct).full;
552
- return {
618
+ stabRuns.push({
553
619
  label: off === 0 ? 'база' : off < H ? `фаза +${off * BAR_INTERVAL}м` : `старт +${Math.round(off / bpd)}д`,
554
620
  returnPct: rep?.returnPct ?? 0,
555
621
  maxDrawdownPct: rep?.maxDrawdownPct ?? 0,
556
622
  trades: rep?.trades ?? 0,
557
- };
558
- });
623
+ });
624
+ }
559
625
  const stability = summarizeStability(stabRuns, 'phase');
560
626
 
627
+ emit({ stage: 'calc', pct: 98, text: `Финальный пересчёт · запись ${results[chosen].sim.trades.length} сделок и кривых капитала в базу…` });
628
+ await tick();
629
+ const chosenTrades = results[chosen].sim.trades;
630
+ for (let ti = 0; ti < chosenTrades.length; ti++) {
631
+ saveTrade(runId, chosenTrades[ti]);
632
+ if ((ti & 511) === 511) await tick();
633
+ }
634
+
561
635
  saveRunExtra(runId, {
562
636
  kind: 'model',
563
637
  symbols: tokens.map((t) => t.symbol),
@@ -625,8 +699,8 @@ async function runStabilityLoop(targetId, runRow, st) {
625
699
  const shifts = [-8, -4, 0, 4, 8];
626
700
  const H = barsFromHours(p.modelHorizon);
627
701
  const tillTs = Date.now();
628
- // Используем округление до часа для совпадения ключа кэша датасета в памяти!
629
- const fromTs = tillTs - p.modelHistoryDays * DAY - Math.ceil(p.modelTrainDays * 1.5 + 20) * DAY;
702
+ const todayStartMsk = Math.floor((tillTs + MSK) / DAY) * DAY - MSK;
703
+ const fromTs = todayStartMsk - 1650 * DAY;
630
704
  const periodTill = runRow.metrics?.period?.till || tillTs;
631
705
  const emitStab = (m) => { if (!st.cancelled) hub.emit({ type: 'stability:progress', runId: targetId, ...m }); };
632
706
  emitStab({ pct: 2, text: 'Проверка устойчивости: чтение локальных цен…' });
@@ -2,27 +2,27 @@
2
2
  // Описание всех настроек: для формы в интерфейсе, для ИИ-стратега и для проверки значений.
3
3
  // hidden — не показывать в интерфейсе; ai:false — ИИ менять запрещено (честность проверки).
4
4
  export const PARAMS = [
5
- { key: 'modelAutoAdapt', label: 'Самообучение правил торговли (Авто)', hint: 'Полный автомат: стратегия сама учится на мульти-горизонте (1,5–3,5 ч), автоматически управляет порогами входа и выхода под вашу комиссию, масштабирует слоты по силе сигнала и временно ставит на карантин убыточные акции. Ручная настройка горизонта и порогов не требуется.', type: 'checkbox' },
6
- { key: 'modelTopK', label: 'Сколько акций держать', hint: 'Сколько акций с лучшим прогнозом можно держать одновременно (при продажах — и столько же акций на продажу).', min: 1, max: 20 },
7
- { key: 'modelDirection', label: 'Направление торговли', hint: '«Только в рост (без шорта)» — классическая покупка акций и их продажа при фиксации прибыли (работает на любом счёте и ИИС). «В рост и в падение (с шортом)» — дополнительно зарабатывает на падающих акциях за счёт коротких продаж (требуется включённая маржинальная торговля у брокера; на брокерских счетах без маржиналки шорты отключаются автоматически).', type: 'select', options: [['long', 'Только в рост (без шорта)'], ['longshort', 'В рост и в падение (с шортом)']] },
8
- { key: 'modelHorizon', label: 'Горизонт прогноза, часов (ручной режим)', hint: 'При включённом Авто-режиме управляется автоматически (мульти-горизонт 1,5–3,5 ч). При выключенном Авто — на сколько часов вперёд предсказывать движение.', min: 0.5, max: 8 },
9
- { key: 'modelMinEdge', label: 'Порог выгоды на вход (ручной режим)', hint: 'При включённом Авто-режиме управляется автоматически (2,5× издержек). При выключенном Авто — во сколько раз ожидаемое движение должно превышать стоимость сделки.', min: 0, max: 10 },
10
- { key: 'modelExitRatio', label: 'Порог на выход (ручной режим)', hint: 'При включённом Авто-режиме управляется автоматически (двухступенчатый выход 0,35 / 0,95). При выключенном Авто — до какой доли от порога входа держать позицию.', min: 0, max: 1, step: 0.05 },
11
- { key: 'modelAllocMode', label: 'Распределение суммы между акциями', hint: '«Поровну» — одинаковая доля на каждую позицию; «Обратно размаху акции» — спокойным бумагам больше, волатильным — меньше.', type: 'select', options: [['equal', 'Поровну'], ['invvol', 'Обратно размаху акции']] },
12
- { key: 'modelMarketFilter', label: 'Фильтр падающего рынка', hint: 'Не открывать новые покупки, когда индекс Мосбиржи находится в среднесрочном нисходящем тренде.', type: 'select', options: [['off', 'Выключен'], ['ma', 'Не покупать при падении индекса']] },
13
- { key: 'modelTrainDays', label: 'Окно обучения, торговых дней', hint: 'На скольких последних торговых днях стратегия строит карту рынка и учится перед каждым месяцем прогноза.', min: 30, max: 400 },
14
- { key: 'modelHistoryDays', label: 'Период исследования, дней', hint: 'За сколько последних календарных дней проверять стратегию на истории.', min: 120, max: 1100 },
15
- { key: 'modelEmbedDim', label: 'Размерность карты рынка (эмбеддинг)', hint: 'Сколько скрытых факторов совместного движения описывают каждую акцию на карте рынка.', min: 2, max: 20 },
16
- { key: 'modelTextWeight', label: 'Вес бизнес-профилей компаний на карте', hint: '0 — карта рынка строится только по совместному поведению цен; 0,1…0,3 — примешивает фундаментальное сходство бизнеса компаний от ИИ.', min: 0, max: 1, step: 0.1 },
17
- { key: 'modelUseMacro', label: 'Учитывать нефть, валюту, облигации', hint: 'Дополнительно учитывает чувствительность акций к Brent, юаню, доллару, золоту и RGBI.', type: 'checkbox', hidden: true },
5
+ { key: 'modelAutoAdapt', label: 'Самообучение правил торговли (Авто)', hint: 'Полный автомат: стратегия сама учится на мульти-горизонте (1,5–3,5 ч), сама выбирает число акций (1–4) и размер слота по силе сигнала, управляет порогами входа и выхода и временно ставит на карантин убыточные акции.', type: 'checkbox', hidden: true, fixed: true },
6
+ { key: 'modelTopK', label: 'Сколько акций держать (0 — Авто)', hint: '0 — система сама выбирает от 1 до 4 акций на каждом шаге по силе и концентрации сигналов; 1…20 — верхний предел числа позиций на сторону.', min: 0, max: 20, hidden: true, fixed: true },
7
+ { key: 'modelDirection', label: 'Направление торговли', hint: 'Автоматически подстраивается под тариф и статус маржинальной торговли на брокерском счёте.', type: 'select', options: [['long', 'Только в рост (без шорта)'], ['longshort', 'В рост и в падение (с шортом)']], hidden: true, fixed: true },
8
+ { key: 'modelHorizon', label: 'Горизонт прогноза, часов (ручной режим)', hint: 'Управляется автоматически (мульти-горизонт 1,5–3,5 ч).', min: 0.5, max: 8, hidden: true, fixed: true },
9
+ { key: 'modelMinEdge', label: 'Порог выгоды на вход (ручной режим)', hint: 'Управляется автоматически (2,5× издержек).', min: 0, max: 10, hidden: true, fixed: true },
10
+ { key: 'modelExitRatio', label: 'Порог на выход (ручной режим)', hint: 'Управляется автоматически (двухступенчатый выход 0,35 / 0,95).', min: 0, max: 1, step: 0.05, hidden: true, fixed: true },
11
+ { key: 'modelAllocMode', label: 'Распределение суммы между акциями', hint: 'Управляется автоматически по силе сигнала (Conviction Sizing).', type: 'select', options: [['equal', 'Поровну'], ['invvol', 'Обратно размаху акции']], hidden: true, fixed: true },
12
+ { key: 'modelMarketFilter', label: 'Фильтр падающего рынка', hint: 'Адаптивная балансировка порогов к тренду индекса включена автоматически.', type: 'select', options: [['off', 'Выключен'], ['ma', 'Не покупать при падении индекса']], hidden: true, fixed: true },
13
+ { key: 'modelTrainDays', label: 'Окно обучения, торговых дней', hint: 'Фиксировано на эталонном окне 90 торговых дней.', min: 30, max: 400, hidden: true, fixed: true },
14
+ { key: 'modelHistoryDays', label: 'Период исследования, дней', hint: 'За сколько последних календарных дней проверять стратегию на истории.', min: 120, max: 1100, hidden: true, fixed: true },
15
+ { key: 'modelEmbedDim', label: 'Размерность карты рынка / эмбеддинг (0 — Авто)', hint: 'Авто-калибровка по спектру собственных чисел Марченко–Пастура.', min: 0, max: 1000, hidden: true, fixed: true },
16
+ { key: 'modelTextWeight', label: 'Вес бизнес-профилей компаний на карте', hint: '0 — карта рынка строится только по совместному поведению цен.', min: 0, max: 1, step: 0.1, hidden: true, fixed: true },
17
+ { key: 'modelUseMacro', label: 'Учитывать нефть, валюту, облигации', hint: 'Дополнительно учитывает чувствительность акций к Brent, юаню, доллару, золоту и RGBI.', type: 'checkbox', hidden: true, fixed: true },
18
18
  { key: 'modelType', label: 'Какая стратегия торгует', hint: 'Внимание через карту.', type: 'select', options: [['attention', 'Внимание через карту'], ['leadlag', 'Кто за кем ходит'], ['inertia', 'Своя инерция']], hidden: true, fixed: true },
19
19
  { key: 'modelUseDividends', label: 'Учитывать дивиденды', hint: 'Цены до даты отсечки уменьшаются на размер дивиденда со сверкой по реальному падению цены.', type: 'checkbox', hidden: true, fixed: true },
20
- { key: 'capital', label: 'Стартовая сумма, ₽', hint: 'С какой суммой начинает виртуальный счёт.', min: 10000, max: 1e9, hidden: true },
21
- { key: 'commissionPct', label: 'Комиссия брокера и биржи, % за сделку', hint: 'Сколько берут брокер и биржа за покупку и отдельно за продажу.', min: 0, max: 1, ai: false },
22
- { key: 'slippagePct', label: 'Проскальзывание при исполнении, %', hint: 'На сколько реальная цена сделки в стакане обычно хуже ожидаемой.', min: 0, max: 1, ai: false },
23
- { key: 'modelShortFeePct', label: 'Плата брокеру за заём акций (шорт), % годовых', hint: 'Ставка брокера за удержание коротких позиций (начисляется пропорционально времени удержания сделки).', min: 0, max: 100 },
24
- { key: 'autopilotBudget', label: 'Лимит исследований автопилота за задание', hint: 'Сколько экспериментов Стратег может провести подряд за одно задание в режиме «⚡ Авто».', min: 1, max: 30, step: 1, int: true, ai: false },
25
- { key: 'paperDailyReview', label: 'ИИ разбирает каждый торговый день', hint: 'После окончания торгового дня Стратег сам разбирает сделки виртуального счёта и пишет сводку в чат.', type: 'checkbox' },
20
+ { key: 'capital', label: 'Стартовая сумма, ₽', hint: 'С какой суммой начинает счёт.', min: 10000, max: 1e9, hidden: true },
21
+ { key: 'commissionPct', label: 'Комиссия брокера и биржи, % за сделку', hint: 'Сколько берут брокер и биржа за покупку и отдельно за продажу.', min: 0, max: 1, ai: false, hidden: true, fixed: true },
22
+ { key: 'slippagePct', label: 'Проскальзывание при исполнении, %', hint: 'На сколько реальная цена сделки в стакане обычно хуже ожидаемой.', min: 0, max: 1, ai: false, hidden: true, fixed: true },
23
+ { key: 'modelShortFeePct', label: 'Плата брокеру за заём акций (шорт), % годовых', hint: 'Ставка брокера за удержание коротких позиций (начисляется пропорционально времени удержания сделки).', min: 0, max: 100, hidden: true, fixed: true },
24
+ { key: 'autopilotBudget', label: 'Лимит исследований автопилота за задание', hint: 'Сколько экспериментов Стратег может провести подряд за одно задание.', min: 1, max: 30, step: 1, int: true, ai: false, hidden: true },
25
+ { key: 'paperDailyReview', label: 'ИИ разбирает каждый торговый день', hint: 'После окончания торгового дня Стратег сам разбирает сделки и пишет сводку в чат.', type: 'checkbox', hidden: true },
26
26
  { key: 'newsEnabled', label: 'Следить за новостями', hint: 'Раз в 10 минут собирает новости и раскрытия компаний.', type: 'checkbox', hidden: true, fixed: true },
27
27
  { key: 'newsFilter', label: 'Блокировать сделки против важных свежих новостей', hint: 'В живой торговле и брокерских заявках не покупать бумагу на сильном негативе и не шортить на сильном позитиве за последние 6 часов.', type: 'checkbox' },
28
28
  { key: 'oosPct', label: 'Контрольная часть, %', hint: 'Доля периода в конце, не используемая для подбора настроек.', min: 0, max: 60, ai: false, hidden: true },