neuroalpha 0.6.10 → 0.6.12

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.
@@ -19,11 +19,44 @@ export const STRATEGY_POLICY = Object.freeze({
19
19
  profitLock: true,
20
20
  // размер позиции: 'slot' — доля бюджета × сила сигнала
21
21
  sizing: Object.freeze({ mode: 'slot' }),
22
- // Множитель размера позиции по эшелонам. Для 1-го эшелона множитель равен плечу счёта (настройка maxLeverage),
23
- // 2–3-й эшелон торгуется половинным размером. Проверка 06.11.2026: сделки 1-го эшелона — PF 1,9/3,6 по половинам периода
24
- // (издержки 0,065% на сторону), 2–3-го эшелона — чистыми 0,1% на сделку (издержки 0,125%). Плечо 3 на 1-й эшелон и ½ на 2-й:
25
- // CAGR 56%/49% по половинам против 44%/24% (с доходом LQDT), PF 1,87; рост итога с плечом почти линейный на обеих половинах.
26
- sizeMultT2: 0.5,
22
+ // Адаптивное распределение размера позиции, сублинейное плечо и перелив капитала (water-filling + потеснение):
23
+ // • 1-й эшелон (33 ликвидные акции, 0 стопов на 59 051 свече): живой Conviction Sizing (базовый слот 0,56 × conv до 1,92×)
24
+ // разгоняет одиночные сильные импульсы до потолка 92% бюджета в лонг и 70% в шорт на 1× (убирая холостой простой кэша,
25
+ // т.к. 81,5% активного времени открыта 1 позиция). С ростом плеча счёта потолок одной бумаги растёт сублинейно
26
+ // (1 + 0,80 × (lev − 1): до 166% на 2× и 239% на 3×), оставляя остаток бесплатного плеча под залог LQDT свободным резервом;
27
+ // • 2-й эшелон: единый прямой лимит без двойного перемножения — до 17% бюджета в лонг (масштаб плеча 0,72) и до 7% в шорт (0,40),
28
+ // под жёстким отсечением по реальной ёмкости стакана (15% от медианного 10-минутного оборота свечи barTurnoverRub);
29
+ // • waterFill: при пакетном входе мелкие по лимиту сделки 2-го эшелона не «замораживают» квоту крупных лидеров 1-го эшелона;
30
+ // • crowdOut: двухступенчатое потеснение (ротация остывшей позиции или частичное подрезание без потери peakPrice/entryTs)
31
+ // при нехватке лимита под сильный сигнал 1-го эшелона (повышение качества T2 → T1 либо перекладка из остывшей T1).
32
+ // Проверка 13.11.2026 (06.2024–11.2026, 59 051 свеча): улучшает итог и PF на обеих половинах истории при любом плече и тарифе
33
+ // • плечо 1×: CAGR 59,9%/38,7% по половинам (весь 48,7%, PF 1,90, DD 3,4%) против 49,2%/30,7% (весь 39,5%, PF 1,85);
34
+ // • плечо 2×: CAGR 95,0%/62,4% по половинам (весь 77,6%, 6 мес. 165,1%, PF 2,05, DD 5,6%) против 73,8%/47,4% (весь 59,8%, PF 2,03);
35
+ // • плечо 3×: CAGR 133,7%/90,1% по половинам (весь 110,3%, 6 мес. 292,3%, PF 2,18, DD 7,1%) против 104,6%/65,6% (весь 83,7%, PF 2,07).
36
+ slotOwner: 0.56,
37
+ convMaxOwner: 1.92,
38
+ convKOwner: 0.38,
39
+ capT1Long: 0.92,
40
+ capT1Short: 0.70,
41
+ capT2Long: 0.17,
42
+ capT2Short: 0.070,
43
+ slotT2LongMult: 0.25,
44
+ slotT2ShortMult: 0.10,
45
+ levSlopeT1: 0.80,
46
+ levSlopeT2Long: 0.72,
47
+ levSlopeT2Short: 0.40,
48
+ turnoverCapMult: 0.15,
49
+ tpRetainT2: 0.75,
50
+ tpTrailT2: 0.35,
51
+ netPickSort: true,
52
+ waterFill: true,
53
+ crowdOut: true,
54
+ crowdMinMin: 20,
55
+ crowdMinPct: 0.06,
56
+ crowdRotBufPct: 0.08,
57
+ crowdTrimBufPct: 0.12,
58
+ crowdCoolRatioT1: 0.45,
59
+ crowdCoolRatioT2: 0.80,
27
60
  // капитал хранится в фонде денежного рынка LQDT как залог: внутридневные позиции открываются под обеспечение
28
61
  // (без платы за перенос — позиции закрываются до ночи), LQDT не продаётся под входы
29
62
  cashYield: true,
@@ -63,12 +96,15 @@ export function sessionEdgeWarning(ts = Date.now()) {
63
96
  // ---------- фиксация прибыли ----------
64
97
  // Уровни в процентах выигрыша от средней цены. peakGainPct — лучший выигрыш с момента входа.
65
98
  // Возвращает { floorPct, minExitPct, kind } — выход, когда текущий выигрыш опустился до floorPct, но не ниже minExitPct; или null.
66
- export function profitLockLevels(sym, peakGainPct, heldMin, roundTripFeePct = 0.10) {
99
+ export function profitLockLevels(sym, peakGainPct, heldMin, roundTripFeePct = 0.10, policy = STRATEGY_POLICY) {
100
+ const isT1 = TIER1_LIQUID_SYMBOLS.has(sym);
67
101
  const tierMult = symbolEdgeMult(sym);
68
102
  const minNetFloorPct = Math.max(roundTripFeePct + 0.30, Math.round(0.38 * tierMult * 100) / 100);
69
103
  const tpTriggerPct = (heldMin < 15 ? 1.55 : 1.20) * tierMult;
70
104
  if (peakGainPct >= tpTriggerPct) {
71
- return { floorPct: Math.max(peakGainPct * 0.70, peakGainPct - 0.45, minNetFloorPct), minExitPct: minNetFloorPct, kind: 'tp' };
105
+ const retain = !isT1 && policy?.tpRetainT2 ? policy.tpRetainT2 : 0.70;
106
+ const trail = !isT1 && policy?.tpTrailT2 ? policy.tpTrailT2 : 0.45;
107
+ return { floorPct: Math.max(peakGainPct * retain, peakGainPct - trail, minNetFloorPct), minExitPct: minNetFloorPct, kind: 'tp' };
72
108
  }
73
109
  if (heldMin >= 15 && peakGainPct >= 0.85 * tierMult) {
74
110
  return { floorPct: Math.max(minNetFloorPct, peakGainPct * 0.45), minExitPct: roundTripFeePct + 0.22, kind: 'be' };
@@ -121,31 +157,166 @@ export function exitHoldRatio(heldMin, horizonMin, { sigmaBarPct = 0, realizedBa
121
157
  return startRatio + (endRatio - startRatio) * ramp;
122
158
  }
123
159
 
124
- // ---------- плечо и множители размера по эшелонам ----------
160
+ // ---------- профиль слотов по тарифу, плечо и предельный размер позиции ----------
125
161
  export const MAX_LEVERAGE = 7;
126
- export function tierSizeMult(symbol, leverage = 1, policy = STRATEGY_POLICY) {
162
+
163
+ export function planSizingProfile(plan = 'owner', dir = 'longshort', policy = STRATEGY_POLICY) {
164
+ const id = typeof plan === 'object' && plan ? (plan.isOwner ? 'owner' : (plan.id || 'investor')) : String(plan || 'owner');
165
+ if (id === 'owner') {
166
+ return {
167
+ id: 'owner',
168
+ slotW: policy?.slotPct ?? policy?.slotOwner ?? 0.56,
169
+ convMax: policy?.convMaxOwner ?? 1.92,
170
+ convK: policy?.convKOwner ?? 0.38,
171
+ capT1Long: policy?.capT1Long ?? 0.92,
172
+ capT1Short: policy?.capT1Short ?? 0.70,
173
+ capT2Long: policy?.capT2Long ?? 0.17,
174
+ capT2Short: policy?.capT2Short ?? 0.070,
175
+ };
176
+ }
177
+ if (id === 'vip') {
178
+ return {
179
+ id: 'vip',
180
+ slotW: policy?.slotPct ?? 0.46,
181
+ convMax: 1.50,
182
+ convK: 0.28,
183
+ capT1Long: 0.75,
184
+ capT1Short: 0.56,
185
+ capT2Long: 0.14,
186
+ capT2Short: 0.065,
187
+ };
188
+ }
189
+ if (id === 'pro') {
190
+ return {
191
+ id: 'pro',
192
+ slotW: policy?.slotPct ?? (dir === 'long' ? 0.36 : 0.34),
193
+ convMax: 1.0,
194
+ convK: 0,
195
+ capT1Long: 0.52,
196
+ capT1Short: 0.40,
197
+ capT2Long: 0.12,
198
+ capT2Short: 0.060,
199
+ };
200
+ }
201
+ return {
202
+ id: 'investor',
203
+ slotW: policy?.slotPct ?? 0.42,
204
+ convMax: 1.0,
205
+ convK: 0,
206
+ capT1Long: 0.58,
207
+ capT1Short: 0,
208
+ capT2Long: 0.14,
209
+ capT2Short: 0,
210
+ };
211
+ }
212
+
213
+ export function convictionMult(edgeRatio = 1, profile = null) {
214
+ const convMax = profile?.convMax ?? 1;
215
+ const convK = profile?.convK ?? 0;
216
+ if (!(convMax > 1) || !(edgeRatio > 0)) return 1;
217
+ return Math.min(convMax, Math.max(0.85, 0.80 + convK * edgeRatio));
218
+ }
219
+
220
+ // Сублинейный множитель масштабирования одиночной позиции от плеча счёта (на плече 1× равен 1,00):
221
+ // разгружает риск одиночной бумаги на высоких плечах и оставляет свободный маржинальный резерв под параллельные идеи.
222
+ export function tierSizeMult(symbol, leverage = 1, sideOrPolicy = 'long', policyArg = STRATEGY_POLICY) {
223
+ const isObj = sideOrPolicy && typeof sideOrPolicy === 'object';
224
+ const side = isObj ? 'long' : (sideOrPolicy === 'short' ? 'short' : 'long');
225
+ const policy = isObj ? sideOrPolicy : (policyArg || STRATEGY_POLICY);
127
226
  const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
128
- return TIER1_LIQUID_SYMBOLS.has(symbol) ? lev : (policy.sizeMultT2 ?? 1);
227
+ const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
228
+ // Обратная совместимость со старыми вариантами лаборатории (sizeMultT2 / sizeMultT2LongBase)
229
+ if (policy?.sizeMultT2 !== undefined) return isT1 ? (policy.sizeMultT1 ?? lev) : policy.sizeMultT2;
230
+ if (policy?.sizeMultT2LongBase !== undefined) {
231
+ if (isT1) return policy.sizeMultT1 ?? lev;
232
+ const rawOld = side === 'short'
233
+ ? Math.max(policy.sizeMultT2ShortBase ?? 0.35, (policy.sizeMultT2ShortLev ?? 0.22) * lev)
234
+ : Math.max(policy.sizeMultT2LongBase ?? 0.50, (policy.sizeMultT2LongLev ?? 0.38) * lev);
235
+ return Math.round(rawOld * 1000) / 1000;
236
+ }
237
+ if (isT1 && policy?.sizeMultT1 !== undefined) return policy.sizeMultT1;
238
+ const slope = isT1
239
+ ? (policy?.levSlopeT1 ?? 0.80)
240
+ : (side === 'short' ? (policy?.levSlopeT2Short ?? 0.40) : (policy?.levSlopeT2Long ?? 0.72));
241
+ return Math.round((1 + slope * (lev - 1)) * 1000) / 1000;
129
242
  }
130
243
 
131
244
  // ---------- предельный размер позиции на одну бумагу ----------
132
- export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0) {
245
+ export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY, profile = null) {
133
246
  const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
134
- const rawCap = budget * (side === 'short' ? (isT1 ? 0.35 : 0.18) : (isT1 ? 0.45 : 0.22));
135
- return (!isT1 && barTurnoverRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnoverRub * 0.12)) : rawCap;
247
+ const isShort = side === 'short';
248
+ const legacyMode = policy?.sizeMultT2 !== undefined || policy?.sizeMultT2LongBase !== undefined;
249
+ let capPct;
250
+ if (legacyMode) {
251
+ capPct = isShort
252
+ ? (isT1 ? (policy?.capT1Short ?? 0.38) : (policy?.capT2Short ?? 0.18))
253
+ : (isT1 ? (policy?.capT1Long ?? 0.50) : (policy?.capT2Long ?? 0.22));
254
+ } else if (profile) {
255
+ capPct = isShort
256
+ ? (isT1 ? profile.capT1Short : profile.capT2Short)
257
+ : (isT1 ? profile.capT1Long : profile.capT2Long);
258
+ } else {
259
+ capPct = isShort
260
+ ? (isT1 ? (policy?.capT1Short ?? 0.70) : (policy?.capT2Short ?? 0.070))
261
+ : (isT1 ? (policy?.capT1Long ?? 0.92) : (policy?.capT2Long ?? 0.17));
262
+ }
263
+ const rawCap = budget * capPct;
264
+ const turnMult = policy?.turnoverCapMult ?? 0.15;
265
+ return (!isT1 && barTurnoverRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnoverRub * turnMult)) : rawCap;
136
266
  }
137
267
 
138
- // Размер новой позиции в рублях до учёта свободного кэша и пропорциональной доли:
139
- // 'slot' — рабочий слот × сила сигнала (conv), не больше предела бумаги;
268
+ // Размер новой позиции в рублях до учёта свободного лимита и итеративного перелива (water-filling):
269
+ // 'slot' — рабочий слот × множитель эшелона × сила сигнала (conv), не больше предела бумаги;
140
270
  // 'risk' — при срабатывании стопа счёт теряет не больше riskPct% бюджета.
141
- export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0 }, policy = STRATEGY_POLICY) {
142
- const cap = symbolCapRub(symbol, side, budget, barTurnoverRub);
143
- const sz = policy.sizing || {};
271
+ export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0, profile = null }, policy = STRATEGY_POLICY) {
272
+ const cap = symbolCapRub(symbol, side, budget, barTurnoverRub, policy, profile);
273
+ const sz = policy?.sizing || {};
144
274
  if (sz.mode === 'risk' && stopPct > 0) {
145
275
  const byRisk = (budget * (sz.riskPct / 100)) / (stopPct / 100);
146
276
  return Math.min(cap, byRisk * (sz.useConv ? conv : 1), sz.maxSlotPct ? budget * sz.maxSlotPct : Infinity);
147
277
  }
148
- return Math.min(cap, slotRub * conv);
278
+ const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
279
+ const legacyMode = policy?.sizeMultT2 !== undefined || policy?.sizeMultT2LongBase !== undefined;
280
+ const slotTierMult = (isT1 || legacyMode)
281
+ ? 1.0
282
+ : (side === 'short' ? (policy?.slotT2ShortMult ?? 0.10) : (policy?.slotT2LongMult ?? 0.25));
283
+ return Math.min(cap, slotRub * slotTierMult * conv);
284
+ }
285
+
286
+ // Итеративное распределение доступного лимита методом перелива (water-filling):
287
+ // бумаги с низким индивидуальным потолком (2-й эшелон / лимит оборота стакана) забирают ровно свой потолок,
288
+ // а весь невыбранный остаток капитала перераспределяется между флагманскими лидерами пропорционально чистой выгоде (weight).
289
+ export function waterFillAllocations(items, availRub) {
290
+ const n = Array.isArray(items) ? items.length : 0;
291
+ const out = new Float64Array(n);
292
+ if (!(availRub > 0) || !n) return out;
293
+ const totalDesired = items.reduce((s, x) => s + Math.max(0, Number(x.targetRub) || 0), 0);
294
+ if (totalDesired <= availRub) {
295
+ for (let k = 0; k < n; k++) out[k] = Math.max(0, Number(items[k].targetRub) || 0);
296
+ return out;
297
+ }
298
+ let remAvail = availRub;
299
+ const open = new Set(items.map((_, idx) => idx));
300
+ while (open.size > 0 && remAvail > 0) {
301
+ let wSum = 0;
302
+ for (const idx of open) wSum += Math.max(1e-6, Number(items[idx].weight) || 0);
303
+ let clampedAny = false;
304
+ for (const idx of [...open]) {
305
+ const share = remAvail * (Math.max(1e-6, Number(items[idx].weight) || 0) / wSum);
306
+ const cap = Math.max(0, Number(items[idx].targetRub) || 0);
307
+ if (cap <= share + 1e-6) {
308
+ out[idx] = cap;
309
+ remAvail = Math.max(0, remAvail - cap);
310
+ open.delete(idx);
311
+ clampedAny = true;
312
+ }
313
+ }
314
+ if (!clampedAny) {
315
+ for (const idx of open) out[idx] = remAvail * (Math.max(1e-6, Number(items[idx].weight) || 0) / wSum);
316
+ break;
317
+ }
318
+ }
319
+ return out;
149
320
  }
150
321
 
151
322
  // ---------- контекст торгового дня бумаги по свечам до момента решения ----------
@@ -40,7 +40,9 @@ export { isMarketDown };
40
40
  // 16: главная оценка — симулятор с правилами живого контура (simulator.js), аварийный стоп 7%
41
41
  // 17: капитал в LQDT как залог, половинный размер 2-го эшелона, строки с плечом 3 и 5
42
42
  // 18: глубина сопровождения позиции и вечернего окна входов holdBars=13 (130 мин) во всех контурах симуляции
43
- const DATA_VERSION = 18;
43
+ // 19: ревизия правил 1-го и 2-го эшелонов (MTLR/BSPB/RTKM/SVCB в T1, асимметричный масштаб T2 от плеча, лимит 50%/38% T1 и плотный трал T2)
44
+ // 20: разблокировка Conviction Sizing в T1 (до 92%/70% с сублинейным плечом 0,80), прямой риск-лимит T2, перелив water-filling и двухступенчатое потеснение
45
+ const DATA_VERSION = 20;
44
46
 
45
47
  const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
46
48
  function reuseParamsKey(params) {
@@ -821,7 +823,7 @@ async function run(runId, p, state) {
821
823
  const lqdtRows = await ensureLqdtHistory().catch(() => []);
822
824
  const lqdt = lqdtRows.length ? alignSeries(timeline, lqdtRows) : null;
823
825
  const liveRows = [];
824
- for (const lev of [1, 3, 5]) {
826
+ for (const lev of [1, 2, 3, 5]) {
825
827
  const lp = simulateStrategy({ ds, wf: wfLive, p, capital: p.capital, shortable: shortableSet(), divCal, lqdt, leverage: lev });
826
828
  const yrs = Math.max(1 / 365, ((lp.curve.at(-1)?.ts || 0) - (lp.curve[0]?.ts || 0)) / (365.25 * 86400000));
827
829
  const lastLpTs = lp.curve.at(-1)?.ts || 0;
@@ -836,7 +838,7 @@ async function run(runId, p, state) {
836
838
  liveRows.push({
837
839
  key: lev === 1 ? 'liveRules' : `liveRulesL${lev}`,
838
840
  label: lev === 1 ? 'Правила живого контура, без плеча (главная оценка)' : `Правила живого контура, плечо ${lev}`,
839
- note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы, размер позиции и пределы на бумагу, издержки по эшелонам${lqdt ? ', капитал в LQDT как залог' : ''}${lev > 1 ? `; позиции 1-го эшелона ×${lev}` : ''}`,
841
+ note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы, итеративный перелив (water-filling), двухступенчатое потеснение и издержки по эшелонам${lqdt ? ', капитал в LQDT как залог' : ''}${lev > 1 ? `; сублинейный масштаб одиночной позиции при лимите портфеля ×${lev}` : ''}`,
840
842
  full: { returnPct: full.returnPct, cagrPct, profitFactor: full.profitFactor, maxDrawdownPct: full.maxDrawdownPct, trades: full.trades, winRate: full.winRate, avgTradePct: full.avgTradePct, longPnl: round(lp.trades.filter((t) => t.side === 'long').reduce((a, t) => a + t.pnl, 0), 0), shortPnl: round(lp.trades.filter((t) => t.side === 'short').reduce((a, t) => a + t.pnl, 0), 0), afterTaxCagrPct: cagrPct === null ? null : round(cagrPct > 0 ? cagrPct * 0.87 : cagrPct, 1) },
841
843
  costs: null,
842
844
  last12: segOf(lastLpTs - 365 * 86400000),
@@ -17,7 +17,11 @@ import {
17
17
  isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isMarketDown,
18
18
  } from './edge.js';
19
19
  import { nextDividendAt, dividendRules } from './dividends.js';
20
- import { STRATEGY_POLICY, MAX_LEVERAGE, sessionEdgeWarning, profitLockLevels, protectiveStopPct, exitHoldRatio, targetPositionRub, counterTrendBlocked, thesisStopPx, dayContext } from './policy.js';
20
+ import {
21
+ STRATEGY_POLICY, MAX_LEVERAGE, sessionEdgeWarning, profitLockLevels, protectiveStopPct, exitHoldRatio,
22
+ planSizingProfile, convictionMult, tierSizeMult, targetPositionRub, waterFillAllocations,
23
+ counterTrendBlocked, thesisStopPx, dayContext,
24
+ } from './policy.js';
21
25
 
22
26
  // Выравнивание внешнего ряда (свечи IMOEXF, дневные цены LQDT) по шкале датасета с переносом последней цены
23
27
  export function alignSeries(timeline, rows) {
@@ -40,7 +44,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
40
44
  const bpd = Math.max(1, Math.round(ds.barsPerDay || 94));
41
45
  const stepBars = plan === 'owner' || plan === 'vip' ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
42
46
  const K = policy.K ?? 4;
43
- const dir = p.modelDirection === 'long' ? 'long' : 'longshort';
47
+ const dir = plan === 'investor' || p.modelDirection === 'long' ? 'long' : 'longshort';
44
48
  const realCost = (p.commissionPct + p.slippagePct) / 100;
45
49
  const need = 2.5 * 2 * realCost * (policy.entryEdgeMult ?? ENTRY_EDGE_MULT);
46
50
  const roundTripFeePct = 2 * (p.commissionPct + p.slippagePct);
@@ -50,9 +54,15 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
50
54
  const cooldownBars = Math.round((policy.cooldownMin ?? 30) / BAR_INTERVAL);
51
55
  const stopCooldownBars = Math.round((policy.stopCooldownMin ?? 60) / BAR_INTERVAL);
52
56
  const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
53
- const slotW = policy.slotPct ?? (plan === 'owner' ? 0.45 : plan === 'vip' ? 0.40 : plan === 'pro' ? Math.min(0.28, 1.15 / 8) : Math.min(0.24, 1.05 / 5));
54
- const convMax = plan === 'owner' ? 1.65 : plan === 'vip' ? 1.30 : 1;
55
- const convK = plan === 'owner' ? 0.30 : 0.25;
57
+ const legacySizing = policy.sizeMultT2 !== undefined || policy.sizeMultT2LongBase !== undefined;
58
+ const profile = legacySizing
59
+ ? {
60
+ slotW: policy.slotPct ?? (plan === 'owner' ? 0.50 : plan === 'vip' ? 0.40 : plan === 'pro' ? Math.min(0.28, 1.15 / 8) : Math.min(0.24, 1.05 / 5)),
61
+ convMax: plan === 'owner' ? 1.65 : plan === 'vip' ? 1.30 : 1,
62
+ convK: plan === 'owner' ? 0.30 : 0.25,
63
+ }
64
+ : planSizingProfile(plan, dir, policy);
65
+ const slotW = profile.slotW;
56
66
  const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
57
67
  const maxGross = policy.maxGross ?? 0.99 * lev;
58
68
  const capScale = policy.capScale ?? 1;
@@ -69,7 +79,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
69
79
  const curve = [];
70
80
  const cooldownUntil = new Int32Array(N).fill(-1);
71
81
  const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
72
- const stats = { stops: 0, thesis: 0, locks: 0, signalExits: 0, timeExits: 0, night: 0, rotations: 0, regime: 0, div: 0, blockedCT: 0, entries: 0, missedPassive: 0, hedgePnl: 0, hedgeCost: 0, cashYield: 0, exposureSum: 0, exposureN: 0 };
82
+ const stats = { stops: 0, thesis: 0, locks: 0, signalExits: 0, timeExits: 0, night: 0, rotations: 0, trims: 0, regime: 0, div: 0, blockedCT: 0, entries: 0, missedPassive: 0, hedgePnl: 0, hedgeCost: 0, cashYield: 0, exposureSum: 0, exposureN: 0 };
73
83
  let hedgeRub = 0;
74
84
 
75
85
  const divRule = (i, t) => {
@@ -110,6 +120,35 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
110
120
  stockPnl[i] = stockPnl[i] * 0.90 + (g - 2 * c);
111
121
  stockCnt[i] += 1;
112
122
  };
123
+ const trimPos = (i, pos, exitT, exitPx, trimAllocRub, why) => {
124
+ const frac = Math.min(1, Math.max(0, trimAllocRub / pos.alloc));
125
+ if (frac >= 0.999) {
126
+ closePos(i, pos, exitT, exitPx, why, 'rotation');
127
+ stats.rotations++;
128
+ return true;
129
+ }
130
+ const c = costOf(i);
131
+ const g = gainOf(pos, exitPx);
132
+ const sliceAlloc = pos.alloc * frac;
133
+ const sliceEntryFee = pos.entryFee * frac;
134
+ const gross = sliceAlloc * g;
135
+ const fee = (sliceAlloc + (pos.side === 'long' ? gross : -gross)) * c;
136
+ equity += gross - fee;
137
+ if (collectTrades) {
138
+ trades.push({
139
+ symbol: tokens[i].symbol, side: pos.side, entryTs: timeline[pos.entryT], exitTs: timeline[Math.min(exitT, T - 1)],
140
+ entryPrice: pos.entryPx, exitPrice: exitPx, alloc: sliceAlloc,
141
+ pnl: gross - fee - sliceEntryFee, pnlPct: ((gross - fee - sliceEntryFee) / sliceAlloc) * 100,
142
+ commission: fee + sliceEntryFee, barsHeld: (exitT - pos.entryT) * BAR_INTERVAL,
143
+ expectedPct: pos.expectedPct, stopPct: pos.stopPct, reasonExit: why, exitKind: 'trim',
144
+ dayMovePct: pos.dayMovePct, counterTrend: pos.counterTrend, tier: isT1(i) ? 1 : 2,
145
+ });
146
+ }
147
+ pos.alloc -= sliceAlloc;
148
+ pos.entryFee -= sliceEntryFee;
149
+ stats.trims++;
150
+ return false;
151
+ };
113
152
  const mtmUnreal = (t) => {
114
153
  let s = 0;
115
154
  for (const q of held.values()) s += q.alloc * gainOf(q, tokens[q.i].close[t]);
@@ -189,7 +228,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
189
228
  }
190
229
  if (done) continue;
191
230
  if (policy.profitLock) {
192
- const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart, roundTripFeePct);
231
+ const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart, roundTripFeePct, policy);
193
232
  if (lv) {
194
233
  const gO = gainOf(pos, o) * 100;
195
234
  if (gO <= lv.floorPct && gO >= lv.minExitPct) { closePos(i, pos, t, o, 'Фиксация прибыли', 'lock'); stats.locks++; done = true; }
@@ -199,7 +238,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
199
238
  if (done) continue;
200
239
  pos.peakGain = Math.max(pos.peakGain, gainOf(pos, favorable));
201
240
  if (policy.profitLock) {
202
- const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart + BAR_INTERVAL, roundTripFeePct);
241
+ const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart + BAR_INTERVAL, roundTripFeePct, policy);
203
242
  const gC = gainOf(pos, c) * 100;
204
243
  if (lv && gC <= lv.floorPct && gC >= lv.minExitPct) { closePos(i, pos, t, c, 'Фиксация прибыли', 'lock'); stats.locks++; }
205
244
  }
@@ -304,8 +343,84 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
304
343
  let nL = 0, nS = 0;
305
344
  for (const q of held.values()) { if (q.side === 'long') nL++; else nS++; }
306
345
  const budget = equity + mtmUnreal(t);
307
- // ротация остывшей позиции в сильного лидера (на плановом шаге)
308
- if (isStep && (candL.length || candS.length) && grossUsed(t) > budget * maxGross * 0.65) {
346
+ const pickWeight = (i) => (policy.netPickSort !== false ? Math.max(0.0001, Math.abs(edge(i)) - 2 * costOf(i)) : Math.max(0.0001, Math.abs(edge(i))));
347
+ const calcDesiredRub = (i, side, e, stopPct) => {
348
+ const tm = tierSizeMult(tokens[i].symbol, lev, side, policy);
349
+ if (!(tm > 0)) return 0;
350
+ const conv = convictionMult(Math.abs(e) / need, profile);
351
+ return targetPositionRub({
352
+ symbol: tokens[i].symbol, side,
353
+ budget: budget * capScale * tm,
354
+ slotRub: budget * slotW * tm,
355
+ conv, stopPct,
356
+ barTurnoverRub: isT1(i) ? 0 : barTurnover(i, t),
357
+ profile: legacySizing ? null : profile,
358
+ }, policy);
359
+ };
360
+
361
+ // Двухступенчатое динамическое потеснение (ротация остывшей позиции или частичное подрезание T2 под флагман T1)
362
+ let avail = Math.max(0, budget * maxGross - grossUsed(t));
363
+ if (!legacySizing && policy.crowdOut !== false && held.size > 0 && (candL.length || candS.length)) {
364
+ const rawPicks = [
365
+ ...candL.slice(0, Math.max(0, K - nL)).map((i) => [i, 'long']),
366
+ ...candS.slice(0, Math.max(0, K - nS)).map((i) => [i, 'short']),
367
+ ];
368
+ rawPicks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
369
+ const minAgeBars = Math.round((policy.crowdMinMin ?? 20) / BAR_INTERVAL);
370
+ const minChunkRub = Math.max(45000, budget * (policy.crowdMinPct ?? 0.06));
371
+ const rotBuf = (policy.crowdRotBufPct ?? 0.08) / 100;
372
+ const trimBuf = (policy.crowdTrimBufPct ?? 0.12) / 100;
373
+ const desiredItems = [];
374
+ for (const [i, side] of rawPicks) {
375
+ const e = edge(i);
376
+ const sigmaBarPct = sg[i] * 100;
377
+ const ctx = dayContext(ds, i, t, sigmaBarPct);
378
+ if (counterTrendBlocked(side, ctx, policy)) continue;
379
+ const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
380
+ desiredItems.push({ i, side, e, netEdge: pickWeight(i), targetRub: calcDesiredRub(i, side, e, stopPct) });
381
+ }
382
+ const totalDesired = desiredItems.reduce((s, x) => s + x.targetRub, 0);
383
+ let shortfall = totalDesired - avail;
384
+ const bestT1 = desiredItems.find((x) => isT1(x.i));
385
+ if (shortfall >= minChunkRub && bestT1) {
386
+ const elig = [];
387
+ for (const [hi, q] of held) {
388
+ if (t + 1 - q.entryT < minAgeBars) continue;
389
+ if (!(tokens[hi].open[t + 1] > 0)) continue;
390
+ const curSigned = q.side === 'long' ? edge(hi) : -edge(hi);
391
+ const thrEntry = q.side === 'long' ? Math.max(needL * mult[hi] * tierScale(hi), minEdgeL) : Math.max(needS * mult[hi] * tierScale(hi), minEdgeS);
392
+ elig.push({ hi, q, curSigned, thrEntry, tierRank: isT1(hi) ? 1 : 0 });
393
+ }
394
+ elig.sort((a, b) => (a.tierRank - b.tierRank) || (a.curSigned - b.curSigned));
395
+ for (const el of elig) {
396
+ if (shortfall < minChunkRub * 0.5) break;
397
+ const { hi, q, curSigned, thrEntry } = el;
398
+ const pxExit = fill === 'mid' ? (tokens[hi].open[t + 1] + tokens[hi].close[t + 1]) / 2 : tokens[hi].open[t + 1];
399
+ const heldIsT1 = isT1(hi);
400
+ const cooled = curSigned < thrEntry * (heldIsT1 ? (policy.crowdCoolRatioT1 ?? 0.45) : (policy.crowdCoolRatioT2 ?? 0.80));
401
+ const netAdv = bestT1.netEdge - Math.max(0, curSigned);
402
+ if (cooled && netAdv >= rotBuf) {
403
+ if (!heldIsT1 || curSigned < thrEntry * 0.35 || q.alloc - shortfall < minChunkRub) {
404
+ closePos(hi, q, t + 1, pxExit, 'Ротация в сильный сигнал', 'rotation');
405
+ stats.rotations++;
406
+ if (q.side === 'long') nL--; else nS--;
407
+ } else {
408
+ const trimAmt = Math.min(shortfall, q.alloc - minChunkRub);
409
+ if (trimAmt >= minChunkRub) trimPos(hi, q, t + 1, pxExit, trimAmt, 'Частичное потеснение под сильный сигнал');
410
+ }
411
+ avail = Math.max(0, budget * maxGross - grossUsed(t));
412
+ shortfall = totalDesired - avail;
413
+ } else if (!cooled && !heldIsT1 && netAdv >= trimBuf && q.alloc >= minChunkRub * 1.5) {
414
+ const trimAmt = Math.min(shortfall, q.alloc - minChunkRub);
415
+ if (trimAmt >= minChunkRub) {
416
+ trimPos(hi, q, t + 1, pxExit, trimAmt, 'Частичное потеснение под сильный сигнал');
417
+ avail = Math.max(0, budget * maxGross - grossUsed(t));
418
+ shortfall = totalDesired - avail;
419
+ }
420
+ }
421
+ }
422
+ }
423
+ } else if (legacySizing && isStep && (candL.length || candS.length) && grossUsed(t) > budget * maxGross * 0.65) {
309
424
  const best = Math.max(candL.length ? edge(candL[0]) : 0, candS.length ? -edge(candS[0]) : 0);
310
425
  if (best >= need * 2.2) {
311
426
  let wi = -1, we = Infinity;
@@ -321,27 +436,42 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
321
436
  }
322
437
  }
323
438
  }
439
+
324
440
  const picks = [
325
441
  ...candL.slice(0, Math.max(0, K - nL)).map((i) => [i, 'long']),
326
442
  ...candS.slice(0, Math.max(0, K - nS)).map((i) => [i, 'short']),
327
443
  ];
328
- picks.sort((a, b) => Math.abs(edge(b[0])) - Math.abs(edge(a[0])));
329
- let avail = Math.max(0, budget * maxGross - grossUsed(t));
330
- let remW = picks.reduce((s, [i]) => s + Math.max(0.0001, Math.abs(edge(i))), 0);
444
+ picks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
445
+ avail = Math.max(0, budget * maxGross - grossUsed(t));
446
+ const prepared = [];
331
447
  for (const [i, side] of picks) {
332
448
  const e = edge(i);
333
- const w = Math.max(0.0001, Math.abs(e));
334
- const propShare = remW > 0 ? avail * (w / remW) : avail;
335
- remW -= w;
336
449
  const sigmaBarPct = sg[i] * 100;
337
450
  const ctx = dayContext(ds, i, t, sigmaBarPct);
338
451
  if (counterTrendBlocked(side, ctx, policy)) { stats.blockedCT++; continue; }
339
452
  const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
340
- const conv = Math.min(convMax, Math.max(0.85, 0.80 + convK * (Math.abs(e) / need)));
341
- const tm = isT1(i) ? (policy.sizeMultT1 ?? lev) : (policy.sizeMultT2 ?? 1);
342
- if (!(tm > 0)) continue;
343
- const target = targetPositionRub({ symbol: tokens[i].symbol, side, budget: budget * capScale * tm, slotRub: budget * slotW * tm, conv: convMax > 1 ? conv : 1, stopPct, barTurnoverRub: isT1(i) ? 0 : barTurnover(i, t) }, policy);
344
- const alloc = Math.min(target, propShare, avail);
453
+ const targetRub = calcDesiredRub(i, side, e, stopPct);
454
+ if (!(targetRub > 0)) continue;
455
+ prepared.push({ i, side, e, weight: pickWeight(i), targetRub, stopPct, ctx });
456
+ }
457
+ const allocs = (!legacySizing && policy.waterFill !== false)
458
+ ? waterFillAllocations(prepared, avail)
459
+ : (() => {
460
+ const out = new Float64Array(prepared.length);
461
+ let a = avail;
462
+ let remW = prepared.reduce((s, x) => s + x.weight, 0);
463
+ for (let idx = 0; idx < prepared.length; idx++) {
464
+ const propShare = remW > 0 ? a * (prepared[idx].weight / remW) : a;
465
+ remW -= prepared[idx].weight;
466
+ const al = Math.min(prepared[idx].targetRub, propShare, a);
467
+ out[idx] = al;
468
+ a -= al;
469
+ }
470
+ return out;
471
+ })();
472
+ for (let idx = 0; idx < prepared.length; idx++) {
473
+ const { i, side, e, stopPct, ctx } = prepared[idx];
474
+ const alloc = Math.min(allocs[idx], avail);
345
475
  if (!(alloc > 5000)) continue;
346
476
  const tk = tokens[i];
347
477
  let entryPx = fill === 'mid' ? (tk.open[t + 1] + tk.close[t + 1]) / 2 : tk.open[t + 1];
@@ -249,16 +249,31 @@ ${liveTradingSummary}
249
249
  - Если данные инструментов расходятся с календарём или временем — **верь данным инструментов**: это фактические позиции и исполненные сделки.
250
250
 
251
251
  ## Как устроено автоматическое ядро платформы «НейроАльфа» (Рынок как язык)
252
- - **Словарь:** ~100 самых ликвидных акций индекса широкого рынка Мосбиржи (MOEXBMI) + индекс IMOEX. Свечи 10-минутные, дивидендные отсечки скорректированы со сверкой по реальному гэпу цены, объёмы нормированы по 24-часовому суточному профилю (valRatio по часам МСК).
252
+ - **Словарь:** ~125 акций широкого рынка Мосбиржи (MOEXBMI) + индекс IMOEX. Свечи 10-минутные, дивидендные отсечки скорректированы со сверкой по реальному гэпу цены, объёмы нормированы по 24-часовому суточному профилю (valRatio по часам МСК).
253
253
  - **Карта рынка и авто-эмбеддинг (modelEmbedDim = 0 Авто):** окно обучения фиксировано на 90 торговых днях. На каждом шаге размерность карты рынка автоматически определяется по спектральному порогу Марченко–Пастура (отсечение рыночного шума от истинных факторов).
254
- - **12 голов внимания и мульти-горизонт (1,5–3,5 часа):** модель смотрит на 3 масштаба времени (1ч, 3ч, день), цену и денежный поток, узких и широких соседей по карте, предсказывая чистое остаточное движение каждой акции относительно индекса Мосбиржи.
255
- - **Пороги входа и удержания (индивидуальные по эшелонам ликвидности):**
256
- - Базовый порог выгоды на вход = 2,5 × (удвоенная сумма комиссии и проскальзывания) × 1,60 (запас над реальными издержками по стресс-тесту и фактическим сделкам). Для высоколиквидных голубых фишек 1-го эшелона (SBER, LKOH, GAZP, T, YDEX, GMKN, FLOT и др.) при издержках 0,05% за сделку это +0,40% (с индивидуальной подстройкой под оборот и волатильность бумаги).
257
- - Для акций 2–3-го эшелона с более широким спредом в стакане (MRKU, LSRG, SFIN, SOFL, FIXR, NKNCP, SVAV, PIKK, SMLT и др.) порог автоматически повышается в 1,20× — до ~+0,48% (с индивидуальной поправкой на рублёвую ликвидность стакана).
258
- - Авто-балансировка по тренду индекса: на растущем рынке (индекс выше 20-дневной средней) покупки открываются от базового порога (+0,40% 1-й эшелон и +0,48% 2–3-й эшелон), а контртрендовые шорты ужесточаются в 1,25× (до −0,50% для 1-го эшелона и −0,60% для 2–3-го эшелона). На падающем рынке — наоборот. Всегда называй пользователю именно индивидуальный порог конкретной бумаги (\`порог_покупки_проц\` / \`порог_шорта_проц\`).
259
- - Удержание позиции: первые 65 минут после входа действует мягкий порог удержания (0,35× от порога входа), затем — плотный порог фиксации прибыли.
260
- - Реальные издержки на счёте Т-Банка (по фактическим сделкам): комиссия ~0,04% + отклонение цены исполнения от цены сигнала ~0,03–0,08% на сторону.
261
- - **Dynamic Top-K (modelTopK = 0 Авто) и Conviction Sizing:** фиксированного числа акций нет — робот держит от 0 до 4 лонгов и до 4 шортов в зависимости от силы сигналов и концентрирует слот в лучших лидерах.
254
+ - **12 голов внимания и мульти-горизонт обучения (2,5 часа = 15 свечей, диапазон 1,5–3,5 часа):** модель смотрит на 3 масштаба времени (1ч, 3ч, день), цену и денежный поток, узких и широких соседей по карте, предсказывая чистое остаточное движение каждой акции относительно индекса Мосбиржи (Edge_i = y_i × sigma_i × √15).
255
+ - **Эшелоны ликвидности и реальные издержки («на сторону» и «за полный круг»):**
256
+ - «Издержки на сторону» — это потери на одной операции (отдельно на входе и отдельно на выходе); за полный круг сделки («купил → продал») они удваиваются. Комиссия брокера (Т-Банк) одинаковая для всех бумаг (0,04% на сторону = 0,08% за круг), а потери в биржевом стакане (спред + проскальзывание) зависят от эшелона:
257
+ - **1-й эшелон (33 ликвидные акции с глубоким стаканом:** SBER, SBERP, LKOH, GAZP, T, YDEX, ROSN, NVTK, GMKN, PLZL, VTBR, TATN, TATNP, SNGS, SNGSP, ALRS, CHMF, NLMK, MAGN, MOEX, OZON, PHOR, AFLT, MTSS, IRAO, TRNFP, RUAL, FLOT, X5, MTLR, BSPB, RTKM, SVCB): потери в стакане ~0,025% + комиссия 0,04% = **0,065% на сторону (0,13% за круг)**.
258
+ - **2–3-й эшелон (остальные ~92 акции,** включая высоковолатильные бумаги с более широким спредом и хвостовым риском: SMLT, PIKK, POSI, AFKS, VKCO, SPBE, UGLD, HEAD, CBOM, LSRG, SFIN, FESH и др.): потери в стакане ~0,085% + комиссия 0,04% = **0,125% на сторону (0,25% за круг)**.
259
+ - **Пороги входа по эшелонам и тренду индекса:**
260
+ - Базовый порог выгоды на вход = 2,5 × 2 × (комиссия + проскальзывание) × 1,60. Для 1-го эшелона при настройке издержек 0,05% базовый порог составляет **+0,40%** (с тонкой поправкой symbolEdgeMult на оборот и волатильность бумаги в коридоре 0,88..1,45).
261
+ - Для 2–3-го эшелона порог входа повышен в **1,20× (TIER2_EDGE_MULT)** — до **~+0,48%**, чтобы ожидаемый импульс с запасом перекрывал более широкий спред стакана. В первые 20 минут утренней сессии (07:00–07:20 МСК) шорты 2-го эшелона блокируются из-за пустого утреннего стакана.
262
+ - Авто-балансировка по тренду индекса (isMarketDown): на растущем рынке покупки открываются от базового порога (+0,40% 1-й эшелон и +0,48% 2-й эшелон), а контртрендовые шорты ужесточаются в 1,25× (до −0,50% и −0,60%). На падающем рынке — наоборот: покупки ужесточаются в 1,25×, а шорты облегчаются в 0,90×. Всегда называй пользователю точный индивидуальный порог конкретной бумаги (\`порог_покупки_проц\` / \`порог_шорта_проц\`).
263
+ - **Размер позиций, сублинейное плечо, перелив капитала и динамическое потеснение (policy.js):**
264
+ - **Живой Conviction Sizing без холостого резерва:** робот держит до 4 лонгов и до 4 шортов, но поскольку 81,5% активного времени на рынке есть ровно 1 сигнал, стратегия не замораживает половину лимита «на всякий случай». В режиме владельца базовый слот 1-го эшелона составляет 56% бюджета и масштабируется на силу сигнала conv (от 0,85× до 1,92×), разгоняя сильные одиночные импульсы до **92% бюджета в лонг и до 70% в шорт** на счёте 1× (на VIP — до 75% / 56%, на Профи — до 52% / 40%, на Инвесторе — до 58% в лонг).
265
+ - **Сублинейное масштабирование одиночной позиции по плечу (tierSizeMult):**
266
+ 1. В **1-м эшелоне** потолок одной бумаги растёт с плечом счёта сублинейно по формуле 1 + 0,80 × (плечо − 1) (на 1× → 1,00×, на 2× → 1,80×, на 3× → 2,60× при общем лимите портфеля 2×..3× капитала). Это удерживает риск одной бумаги под контролем, а разницу между лимитом бумаги и общим плечом счёта превращает в бесплатный маржинальный резерв под 2-ю и 3-ю параллельные сделки под залог LQDT.
267
+ 2. Во **2-м эшелоне** действует прямой риск-потолок без двойного штрафа: до **17% бюджета в лонг** (масштаб плеча 1 + 0,72 × (плечо − 1)) и до **7% в шорт** (1 + 0,40 × (плечо − 1)), под жёстким отсечением по реальной ликвидности биржи — **не более 15% от медианного 10-минутного оборота свечи (barTurnoverRub)** (минимум 45 000 ₽).
268
+ - **Итеративный перелив капитала (waterFillAllocations) и чистая отдача (netPickSort):** очерёдность и веса между отобранными сделками определяются по чистой ожидаемой отдаче за вычетом круговых издержек (|expectedPct| − 0,13% для 1-го эшелона и |expectedPct| − 0,25% для 2-го). При одновременном входе бумаги с узким лимитом (2-й эшелон / тонкий стакан) забирают ровно свой потолок, а весь невыбранный остаток переливается флагманским лидерам 1-го эшелона.
269
+ - **Двухступенчатое динамическое потеснение (crowdOut):** если свободного лимита не хватает под новый сильный сигнал 1-го эшелона (не моложе 20 минут жизни уже открытых сделок), робот освобождает место по факту: (1) полностью ротирует остывшую позицию либо (2) частично подрезает позицию 2-го эшелона под флагман 1-го эшелона с сохранением времени входа entryTs и накопленного пика peakPrice для трала Profit Lock (на счетах auto работает и на быстрых 20-секундных входах, на confirm — на плановых 30-минутных шагах).
270
+ - **Капитал в фонде денежного рынка LQDT как залог:** свободный капитал работает в LQDT, а внутридневные позиции открываются под обеспечение и закрываются до ночи (без платы брокеру за перенос через ночь).
271
+ - **Глубина сопровождения позиций (130 минут), фиксация прибыли и защитные контуры:**
272
+ - **Глубина сопровождения (holdBars = 13 свечей = 130 минут = 2 ч 10 м):** плановая ревизия портфеля идёт каждые 20–30 минут (holdBars / 5), а вечернее окно новых внутридневных входов закрывается за 130 минут до конца торгов (после **21:30 МСК** новые входы не открываются, перед 23:40–23:50 МСК позиции закрываются перед ночью).
273
+ - **Плавная рампа удержания (exitHoldRatio):** первые **45 минут** после входа действует мягкий порог удержания (0,35× от порога входа), чтобы сделку не выбило шумом; затем с 45-й по **130-ю минуту** порог плавно поднимается к 0,95×, динамически подстраиваясь под сегодняшнюю волатильность бумаги (realizedBarPct / sigmaBarPct в коридоре [0,75..1,25]: в спокойный день держит дольше, в турбулентный фиксирует быстрее).
274
+ - **Скользящая фиксация прибыли (profitLockLevels):** включает безубыток от +0,85% × tierMult (после 15 мин) и скользящий Take-Profit от +1,20% × tierMult (+1,55% × tierMult в первые 15 мин). В 1-м эшелоне трал удерживает ≥70% пиковой прибыли (допуская откат до 0,45%), а во 2-м эшелоне работает **уплотнённый трал — удерживает ≥75% пика и допускает откат не более 0,35%**, забирая импульс до разворота спреда.
275
+ - **Аварийный стоп — 7% от средней цены входа** (выставляется реальной стоп-заявкой у брокера и контролируется внутри свечи).
276
+ - **Карантин убыточных бумаг (isStockQuarantined):** FIFO-учёт кругов сделок с периодом полураспада ~15 торговых дней (0,9985 на 30-минутном шаге); если бумага накопила серию убытков (≥2 сделок с суммарным итогом < −1,2% или < −2,5%), слабые сигналы по ней пропускаются, но **сильный сигнал (≥ 110% от индивидуального порога входа) пробивает карантин**.
262
277
 
263
278
  ## Брокерские счета и режимы работы
264
279
  Полная поддержка — Т-Банк (T-Invest API, включая секцию TQBR и SPBRU для SPBE). Финам, БКС, Алор, Цифра брокер и ITI Capital — бета, только «Подтверждение заявок». СберИнвестиции, ВТБ и Альфа-Инвестиции сейчас не подключаются (нет открытого API для заявок).