neuroalpha 0.5.21 → 0.5.24

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.
@@ -9,7 +9,7 @@ import { ensureProfiles } from './profiles.js';
9
9
  import { spearman } from './linalg.js';
10
10
  import { listRuns, getRun, createRun, finishRun, saveTrade, saveRunExtra, kvGet } from '../db.js';
11
11
  import { strategyDefaults } from '../config.js';
12
- import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight } from './edge.js';
12
+ import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isRegimeShocked } from './edge.js';
13
13
  import { buildDividendCalendar, nextDividendAt, dividendRules, DIV_TAX } from './dividends.js';
14
14
 
15
15
  const DAY = 86400000;
@@ -40,7 +40,9 @@ export function isMarketDown(ds, t) {
40
40
  // 9: исторический состав индекса (без ошибки выжившего), дивиденды префов, правила v0.5.x
41
41
  // 10: запрет переноса внутридневных сделок через ночь, период полураспада карантина 15 дн., строгий 1-й эшелон
42
42
  // 11: индивидуальный расчёт порога каждой акции по её фактическому рублёвому обороту и остаточной волатильности
43
- const DATA_VERSION = 11;
43
+ // 12: калибровка индивидуальных порогов 2–3 эшелона по ликвидности, защита от шорт-сквизов (≥20% за день) и обвалов (≥9,5% за 30м), отбор доступных шортов до Top-K
44
+ // 13: полная докачка левого края истории свечей (раньше при уже накопленных свечах 2022 год не догружался)
45
+ const DATA_VERSION = 13;
44
46
 
45
47
  const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
46
48
  function reuseParamsKey(params) {
@@ -461,8 +463,8 @@ async function run(runId, p, state) {
461
463
  }
462
464
  const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
463
465
  const canOpenAt = (i) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i) && canEnter(i, t);
464
- const okLong = (i) => !divRule(i, t)?.blockLong;
465
- const okShort = (i) => !divRule(i, t)?.blockShort;
466
+ const okLong = (i) => !divRule(i, t)?.blockLong && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
467
+ const okShort = (i) => !divRule(i, t)?.blockShort && (!shortable || shortable.has(tokens[i].symbol)) && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'short'));
466
468
  let order, entries;
467
469
  if (dir !== 'long') {
468
470
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
@@ -474,12 +476,15 @@ async function run(runId, p, state) {
474
476
  const e = edge(i);
475
477
  const barsHeld = tIn - h.entryT;
476
478
  const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
477
- if (!(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn, false, 'Прогноз ослаб');
479
+ const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
480
+ if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) {
481
+ closePos(i, h, tIn, false, shocked ? 'Защита от экстремального сквиза/обвала' : 'Прогноз ослаб');
482
+ }
478
483
  }
479
484
  const needL = autoAdapt && bear ? need * 1.25 : need;
480
485
  const needS = autoAdapt ? (bear ? need * 0.90 : need * 1.25) : need;
481
- const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => canOpenAt(i) && okLong(i) && edge(i) > needL * symMult[i]).sort((a, b) => pr[b] - pr[a]);
482
- const candS = !timeOk ? [] : [...pr.keys()].filter((i) => canOpenAt(i) && okShort(i) && edge(i) < -needS * symMult[i]).sort((a, b) => pr[a] - pr[b]);
486
+ const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
487
+ const candS = !timeOk ? [] : [...pr.keys()].filter((i) => edge(i) < -needS * symMult[i] && canOpenAt(i) && okShort(i)).sort((a, b) => pr[a] - pr[b]);
483
488
  // Вытеснение слабой/остывшей позиции новым мощным лидером (Opportunity Rotation):
484
489
  // если капитал заперт в позиции с ослабевшим ожиданием (< 0.55 * exitNeed), а на текущей свече появился лидер в 2.4 раза сильнее порога,
485
490
  // освобождаем слот под лидера, чтобы ни одна фаза не застревала в слабых бумагах
@@ -516,11 +521,27 @@ async function run(runId, p, state) {
516
521
  const e = edge(i);
517
522
  const barsHeld = tIn - h.entryT;
518
523
  const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
519
- if (!(e > req) || !liquid(i, t)) closePos(i, h, tIn, false);
524
+ const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
525
+ if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn, false, shocked ? 'Защита от экстремального обвала' : '');
520
526
  }
521
- const free = K - held.size;
522
527
  const needL = autoAdapt && bear ? need * 1.25 : need;
523
- const rawCand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => canOpenAt(i) && okLong(i) && edge(i) > needL * symMult[i]).sort((a, b) => pr[b] - pr[a]);
528
+ const rawCand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
529
+ const usedNow = [...held.values()].reduce((s, h) => s + h.alloc, 0);
530
+ if (autoAdapt && usedNow > equity * 0.65 && rawCand.length > 0) {
531
+ const bestNewEdge = edge(rawCand[0]);
532
+ if (bestNewEdge >= need * 2.2) {
533
+ let weakestIdx = -1, weakestE = Infinity;
534
+ for (const [i] of held) {
535
+ const curSignedE = edge(i);
536
+ if (curSignedE < exitNeed * 0.60 && curSignedE < weakestE && bestNewEdge > Math.max(need, curSignedE) * 2.4) {
537
+ weakestE = curSignedE;
538
+ weakestIdx = i;
539
+ }
540
+ }
541
+ if (weakestIdx >= 0) closePos(weakestIdx, held.get(weakestIdx), tIn, false, 'Ротация в более сильного лидера');
542
+ }
543
+ }
544
+ const free = K - held.size;
524
545
  const cand = filterDynamicTopK(rawCand, edge, needL, Math.max(0, free));
525
546
  order = [...held.keys(), ...cand];
526
547
  } else {
@@ -912,7 +933,7 @@ function runStressTest({ simulate, chosen, testTs, tsOf, mcurve, p, H }) {
912
933
  return round(((lo + hi) / 2) * 100, 3);
913
934
  };
914
935
  // Подбор порога входа под реальные издержки: живое исполнение, издержки 0,09% на сторону (комиссия 0,04% + проскальзывание ~0,05%)
915
- const thresholdSweep = [1, 1.5, 1.65, 2, 2.5, 3].map((m) => {
936
+ const thresholdSweep = [1, 1.5, 1.6, 1.65, 2, 2.5, 3].map((m) => {
916
937
  const sim = simulate(chosen, 0.0009, p.modelShortFeePct, 0, { ...LIVE, needMult: m });
917
938
  const f = computeReport(sim.trades, sim.curve, p.capital, 0).full;
918
939
  return { needMult: m, current: m === ENTRY_EDGE_MULT, entryEdgePct: round(2.5 * 2 * base * m * 100, 3), returnPct: f.returnPct, cagrPct: cagr(f.returnPct, yearsOf(sim.curve)), profitFactor: f.profitFactor, maxDrawdownPct: f.maxDrawdownPct, trades: f.trades, avgTradePct: f.avgTradePct, last12Pct: segment(sim, lastTs - 365 * 86400000).returnPct, last6Pct: segment(sim, lastTs - 182 * 86400000).returnPct };
@@ -924,7 +945,7 @@ function runStressTest({ simulate, chosen, testTs, tsOf, mcurve, p, H }) {
924
945
  market: { full: mSeg(-Infinity), last12: mSeg(lastTs - 365 * 86400000), last6: mSeg(lastTs - 182 * 86400000) },
925
946
  shortableKnown: Boolean(shortable),
926
947
  caveats: [
927
- 'Состав рынка — сегодняшний индекс широкого рынка, бумаги отобраны по покрытию данных за весь период (ошибка выжившего не устранена).',
948
+ 'Состав рынка — исторический состав индекса MOEXBMI на каждую дату плюс текущий состав; отсекаются только бумаги с покрытием данных менее 15% периода.',
928
949
  'Константы стратегии подбирались на этой же истории, поэтому «последние 6 и 12 месяцев» не являются честно отложенной выборкой. Честная проверка — торговля после фиксации версии.',
929
950
  'Список бумаг для шорта — текущий, а не на каждую дату.',
930
951
  ],
@@ -974,7 +995,9 @@ async function runStabilityLoop(targetId, runRow, st) {
974
995
  await pause();
975
996
  const p = normalizeParams(runRow.params || {});
976
997
  const shifts = [-8, -4, 0, 4, 8];
977
- const H = barsFromHours(p.modelHorizon);
998
+ const autoAdapt = p.modelAutoAdapt !== false;
999
+ const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
1000
+ const H = barsFromHours(effHorizon);
978
1001
  const tillTs = Date.now();
979
1002
  const todayStartMsk = Math.floor((tillTs + MSK) / DAY) * DAY - MSK;
980
1003
  const fromTs = todayStartMsk - 1650 * DAY;
@@ -991,11 +1014,18 @@ async function runStabilityLoop(targetId, runRow, st) {
991
1014
  const trainLen = Math.round(p.modelTrainDays * bpd), testLen = Math.round(20 * bpd);
992
1015
  const chosen = p.modelType in MODELS ? p.modelType : 'attention';
993
1016
  const realCost = (p.commissionPct + p.slippagePct) / 100;
994
- const K = p.modelTopK, dir = p.modelDirection, slots = dir === 'long' ? K : 2 * K;
995
- const autoAdapt = p.modelAutoAdapt !== false;
1017
+ const rawTopK = Number(p.modelTopK ?? 2);
1018
+ const K = rawTopK > 0 ? rawTopK : 4;
1019
+ const dir = p.modelDirection, slots = dir === 'long' ? K : 2 * K;
996
1020
  const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
997
1021
  const exitRatio = Number(p.modelExitRatio ?? 0.5);
998
1022
  const mktFilter = p.modelMarketFilter === 'ma', invVol = p.modelAllocMode === 'invvol';
1023
+ const baseSymMult = Float64Array.from(tokens, (tk) => symbolEdgeMult(tk.symbol));
1024
+ const divCal = buildDividendCalendar(ds);
1025
+ const divRule = (i, t) => {
1026
+ const nd = nextDividendAt(divCal, i, t);
1027
+ return nd ? dividendRules(nd.daysToEx, nd.y) : null;
1028
+ };
999
1029
  const liquid = (i, t) => tokens[i].liq[t] >= LIQ_MIN && tokens[i].open[t + 1] > 0;
1000
1030
  const canEnter = (i, t) => liquid(i, t) && (!tokens[i].inIndex || tokens[i].inIndex[t] === 1);
1001
1031
  const tsOf = (t) => timeline[Math.min(t, T - 1)];
@@ -1019,7 +1049,7 @@ async function runStabilityLoop(targetId, runRow, st) {
1019
1049
  const maxH = autoAdapt ? Math.round(H * 1.4) : H;
1020
1050
  const folds = Math.max(1, Math.ceil((T - maxH - testStart) / testLen));
1021
1051
  let fi = 0;
1022
- const preds = new Map(), sigmaAt = new Map();
1052
+ const preds = new Map(), sigmaAt = new Map(), multAt = new Map();
1023
1053
  while (testStart < T - maxH - 2) {
1024
1054
  if (st.cancelled) throw new Error('Отменено');
1025
1055
  const testEnd = Math.min(testStart + testLen, T - maxH - 2);
@@ -1028,12 +1058,14 @@ async function runStabilityLoop(targetId, runRow, st) {
1028
1058
  emitStab({ pct, text: `Проверка устойчивости: сдвиг ${si + 1} из ${shifts.length} (${shLabel}), шаг ${fi + 1} из ${folds}…` });
1029
1059
  await tick();
1030
1060
  const f = buildFold(ds, { trainStart, testStart, testEnd, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
1061
+ const foldEdgeMults = computeTokenEdgeMults(tokens, f.sigma, trainStart, testStart);
1031
1062
  const m = await MODELS[chosen].fit(f);
1032
1063
  for (let t = testStart; t < testEnd; t++) {
1033
1064
  const u = t - f.lo;
1034
1065
  if (!f.yok[u]) continue;
1035
1066
  preds.set(t, MODELS[chosen].predict(m, f, u));
1036
1067
  sigmaAt.set(t, f.sigma);
1068
+ multAt.set(t, foldEdgeMults);
1037
1069
  }
1038
1070
  testStart = testEnd;
1039
1071
  fi++;
@@ -1051,7 +1083,7 @@ async function runStabilityLoop(targetId, runRow, st) {
1051
1083
  const stockAllowed = (i) => !autoAdapt || !isStockQuarantined(stockCnt[i], stockPnl[i]);
1052
1084
  const curExitRatio = autoAdapt ? 0.95 : exitRatio;
1053
1085
  const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
1054
- const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : Math.min(0.35, 1.4 / (2 * K));
1086
+ const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
1055
1087
  let closedThisBar = new Set();
1056
1088
  const closePos = (i, h, t) => {
1057
1089
  const exitPrice = px(i, t);
@@ -1075,9 +1107,14 @@ async function runStabilityLoop(targetId, runRow, st) {
1075
1107
  for (let i = 0; i < N; i++) { stockPnl[i] *= QUARANTINE_DECAY; stockCnt[i] *= QUARANTINE_DECAY; }
1076
1108
  }
1077
1109
  const pr = preds.get(t), sg = sigmaAt.get(t);
1110
+ const symMult = multAt.get(t) || baseSymMult;
1078
1111
  const bear = isMarketDown(ds, t);
1079
1112
  const mktDown = mktFilter && bear;
1080
1113
  const barTs = tsOf(t);
1114
+ for (const [i, h] of [...held]) {
1115
+ const r = divRule(i, t);
1116
+ if (r && (h.side === 'long' ? r.exitLong : r.exitShort)) closePos(i, h, tIn);
1117
+ }
1081
1118
  if (autoAdapt) {
1082
1119
  for (const [i, h] of [...held]) {
1083
1120
  if (shouldExitBeforeNight(barTs, tsOf(h.entryT), stepBars, BAR_INTERVAL)) closePos(i, h, tIn);
@@ -1085,29 +1122,64 @@ async function runStabilityLoop(targetId, runRow, st) {
1085
1122
  }
1086
1123
  const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
1087
1124
  const canOpenAt = (i) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i) && canEnter(i, t);
1125
+ const okLong = (i) => !divRule(i, t)?.blockLong && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
1126
+ const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || !isRegimeShocked(ds, i, t, 'short'));
1088
1127
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
1089
1128
  const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio, earlyExitNeed = need * 0.35;
1090
1129
  let order, entries;
1091
1130
  if (dir !== 'long') {
1092
1131
  for (const [i, h] of [...held]) {
1093
1132
  const e = edge(i), barsHeld = tIn - h.entryT;
1094
- const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symbolEdgeMult(tokens[i].symbol);
1095
- if (!(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
1133
+ const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
1134
+ const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
1135
+ if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
1096
1136
  }
1097
1137
  const needL = autoAdapt && bear ? need * 1.25 : need;
1098
1138
  const needS = autoAdapt ? (bear ? need * 0.90 : need * 1.25) : need;
1099
- const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => canOpenAt(i) && edge(i) > needL * symbolEdgeMult(tokens[i].symbol)).sort((a, b) => pr[b] - pr[a]);
1100
- const candS = !timeOk ? [] : [...pr.keys()].filter((i) => canOpenAt(i) && edge(i) < -needS * symbolEdgeMult(tokens[i].symbol)).sort((a, b) => pr[a] - pr[b]);
1139
+ const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
1140
+ const candS = !timeOk ? [] : [...pr.keys()].filter((i) => edge(i) < -needS * symMult[i] && canOpenAt(i) && okShort(i)).sort((a, b) => pr[a] - pr[b]);
1141
+ const usedNow = [...held.values()].reduce((s, h) => s + h.alloc, 0);
1142
+ if (autoAdapt && usedNow > equity * 0.65 && (candL.length > 0 || candS.length > 0)) {
1143
+ const bestNewEdge = Math.max(candL.length ? edge(candL[0]) : 0, candS.length ? Math.abs(edge(candS[0])) : 0);
1144
+ if (bestNewEdge >= need * 2.2) {
1145
+ let weakestIdx = -1, weakestE = Infinity;
1146
+ for (const [i, h] of held) {
1147
+ const curSignedE = h.side === 'long' ? edge(i) : -edge(i);
1148
+ if (curSignedE < exitNeed * 0.60 && curSignedE < weakestE && bestNewEdge > Math.max(need, curSignedE) * 2.4) {
1149
+ weakestE = curSignedE;
1150
+ weakestIdx = i;
1151
+ }
1152
+ }
1153
+ if (weakestIdx >= 0) closePos(weakestIdx, held.get(weakestIdx), tIn);
1154
+ }
1155
+ }
1101
1156
  const nL = K - countSide('long'), nS = K - countSide('short');
1102
1157
  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)])];
1103
1158
  } else if (effMinEdge > 0) {
1104
1159
  for (const [i, h] of [...held]) {
1105
1160
  const e = edge(i), barsHeld = tIn - h.entryT;
1106
- const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
1107
- if (!(e > req) || !liquid(i, t)) closePos(i, h, tIn);
1161
+ const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
1162
+ const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
1163
+ if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn);
1108
1164
  }
1109
1165
  const needL = autoAdapt && bear ? need * 1.25 : need;
1110
- const cand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => canOpenAt(i) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]).slice(0, Math.max(0, K - held.size));
1166
+ const rawCand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
1167
+ const usedNow = [...held.values()].reduce((s, h) => s + h.alloc, 0);
1168
+ if (autoAdapt && usedNow > equity * 0.65 && rawCand.length > 0) {
1169
+ const bestNewEdge = edge(rawCand[0]);
1170
+ if (bestNewEdge >= need * 2.2) {
1171
+ let weakestIdx = -1, weakestE = Infinity;
1172
+ for (const [i] of held) {
1173
+ const curSignedE = edge(i);
1174
+ if (curSignedE < exitNeed * 0.60 && curSignedE < weakestE && bestNewEdge > Math.max(need, curSignedE) * 2.4) {
1175
+ weakestE = curSignedE;
1176
+ weakestIdx = i;
1177
+ }
1178
+ }
1179
+ if (weakestIdx >= 0) closePos(weakestIdx, held.get(weakestIdx), tIn);
1180
+ }
1181
+ }
1182
+ const cand = rawCand.slice(0, Math.max(0, K - held.size));
1111
1183
  order = [...held.keys(), ...cand];
1112
1184
  } else {
1113
1185
  const cand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => !closedThisBar.has(i) && stockAllowed(i) && pr[i] > 0 && canEnter(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, K);
@@ -1125,11 +1197,25 @@ async function runStabilityLoop(targetId, runRow, st) {
1125
1197
  }
1126
1198
  const usedStart = [...held.values()].reduce((s, h) => s + h.alloc, 0);
1127
1199
  let avail = Math.max(0, equity * 0.99 - usedStart);
1200
+ const activeTotal = Math.max(2, held.size + newEntries.length);
1201
+ const dynSlotWeight = autoAdapt
1202
+ ? Math.min(dir === 'long' ? 0.58 : 0.75, Math.max(maxSlotWeight, (dir === 'long' ? 1.8 : 3.0) / Math.min(slots, activeTotal + 1)))
1203
+ : maxSlotWeight;
1204
+ if (autoAdapt && newEntries.length > 1) {
1205
+ newEntries.sort((a, b) => Math.abs(b[2] ?? 0) - Math.abs(a[2] ?? 0));
1206
+ }
1207
+ const edgeWeights = newEntries.map(([i, , edgeAt]) => {
1208
+ const effE = edgeAt !== null && edgeAt !== undefined ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
1209
+ return Math.max(0.0001, effE);
1210
+ });
1211
+ let remWeightSum = edgeWeights.reduce((a, b) => a + b, 0);
1128
1212
  for (let idx = 0; idx < newEntries.length; idx++) {
1129
- const [i, side, edgeAt] = newEntries[idx];
1130
- const effEdge = edgeAt !== null && edgeAt !== undefined ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
1131
- const conv = autoAdapt && need > 0 && effEdge > 0 ? Math.min(1.20, Math.max(0.85, 0.80 + 0.20 * (effEdge / need))) : 1;
1132
- 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));
1213
+ const [i, side] = newEntries[idx];
1214
+ const effEdge = edgeWeights[idx];
1215
+ const conv = autoAdapt && need > 0 && effEdge > 0 ? Math.min(1.65, Math.max(0.85, 0.80 + 0.30 * (effEdge / need))) : 1;
1216
+ const propShare = autoAdapt && remWeightSum > 0 ? avail * (effEdge / remWeightSum) : avail / Math.max(1, newEntries.length - idx);
1217
+ remWeightSum = Math.max(0, remWeightSum - effEdge);
1218
+ const baseAlloc = autoAdapt ? Math.min(equity * dynSlotWeight * conv, propShare, avail) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
1133
1219
  let alloc = baseAlloc;
1134
1220
  if (invVol && avgInv > 0 && sg[i] > 0) {
1135
1221
  const w = Math.max(0.4, Math.min(1.8, (1 / sg[i]) / avgInv));
@@ -283,6 +283,7 @@ export const TOOLS = {
283
283
  динамика_проц: x.deltaPct || 0,
284
284
  сигнал_покупки: !!x.signal,
285
285
  ...(buyBlockedByDip ? { покупка_заблокирована: `сейчас бумага падает на текущей свече (${x.lastMovePct}%), сработала защита от ловли падающего ножа` } : {}),
286
+ ...(x.buyBlockedByOverheat ? { покупка_заблокирована_перегревом: `бумага уже перегрета (+${x.buyBlockedByOverheat}% сверх рынка за 1–3 дня), сработала защита от покупки на вершине пампа` } : {}),
286
287
  ...(x.buyBlockedByDiv ? { покупка_заблокирована_дивидендами: x.buyBlockedByDiv } : {}),
287
288
  ...(x.buyBlockedByNews ? { покупка_заблокирована_новостями: x.buyBlockedByNews } : {}),
288
289
  ...(x.shortSignal !== undefined ? { сигнал_шорта: !!x.shortSignal } : {}),