neuroalpha 0.6.9 → 0.6.11

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.
@@ -23,7 +23,7 @@
23
23
 
24
24
  Экран разделён на две части (границу между ними можно перетаскивать мышью, а кнопку чата в шапке — сворачивать/разворачивать):
25
25
 
26
- - **Слева — ИИ-агент «Стратег»**: ваш персональный аналитик и помощник. Понимает текст и голосовой ввод (`🎙`). Умеет выводить пошаговую инструкцию по подключению любого из 9 брокеров, находить ваши субсчета по токену и подключать их прямо из диалога, разбирать текущий живой прогноз, новости Мосбиржи и состояние ваших брокерских счетов.
26
+ - **Слева — ИИ-агент «Стратег»**: ваш персональный аналитик и помощник. Понимает текст и голосовой ввод (`🎙`), поддерживает быстрый старт чистой беседы кнопкой **`➕ Новая`** и просмотр сохранённых диалогов (`.md`) по кнопке **`🗂 Архив`**. Умеет выводить пошаговую инструкцию по подключению любого из 9 брокеров, находить ваши субсчета по токену и подключать их прямо из диалога, разбирать текущий живой прогноз, новости Мосбиржи и состояние ваших брокерских счетов.
27
27
  - **Справа — единое рабочее окно с левым меню навигации (`#main-nav`)**, в котором сразу при входе открывается раздел **«Брокерские счета»**:
28
28
  1. **Торговля:**
29
29
  - **Брокерские счета** — управление всеми подключёнными счетами (боевыми и тестовыми).
@@ -65,13 +65,15 @@
65
65
 
66
66
  ---
67
67
 
68
- ## 4. Прогноз сейчас и пороги входа
68
+ ## 4. Прогноз сейчас, эшелоны ликвидности и сопровождение сделок
69
69
 
70
70
  В разделе **«Прогноз сейчас»** каждые **45 секунд** по живым котировкам Мосбиржи обновляются две таблицы: **«Сильнее рынка»** (кандидаты в покупку) и **«Слабее рынка»** (кандидаты в шорт).
71
71
 
72
- - **Порог выгоды на вход (`2,5×` от полных издержек на круг):** чтобы не тратить деньги на комиссии и рыночный шум, сделка открывается только тогда, когда ожидаемый импульс акции в 2,5 раза превышает удвоенную сумму комиссии и проскальзывания (например, при издержках `0,05%` базовый порог составляет **`+0,25%`**).
73
- - **Авто-балансировка по тренду индекса:** когда индекс Мосбиржи растёт (выше своей 20-дневной средней), покупки идут по базовому порогу (`+0,25%`), а вход в контртрендовый шорт ужесточается в `1,25×` (`-0,313%`). На падающем рынке система автоматически облегчает шорты и ужесточает покупки.
74
- - **Удержание позиции:** первые 65 минут после открытия действует мягкий порог удержания (`0,35×`), дающий сделке развиться, после чего включается плотный порог фиксации прибыли (`0,95×`).
72
+ - **Разделение на 1-й и 2-й эшелоны ликвидности:**
73
+ - **1-й эшелон (33 наиболее ликвидные акции Мосбиржи):** низкие потери в стакане (`0,065%` на сторону = `0,13%` за полный круг со всеми комиссиями), базовый порог входа `~+0,40%`, размер позиции масштабируется на полное плечо счёта (до `50%` бюджета в лонг и до `38%` в шорт).
74
+ - **2–3-й эшелон (остальные ~92 акции):** более широкий спред в стакане (`0,125%` на сторону = `0,25%` за полный круг), порог входа повышен в `1,20×` (`~+0,48%`), размер позиции масштабируется от плеча асимметрично (`max(0,50, 0,38 × плечо)` для лонга и `max(0,35, 0,22 × плечо)` для шорта) и ограничен `15%` от реального 10-минутного оборота свечи.
75
+ - **Авто-балансировка по тренду индекса:** когда индекс Мосбиржи растёт, покупки идут по базовому порогу (`+0,40%` / `+0,48%`), а вход в контртрендовый шорт ужесточается в `1,25×` (`−0,50%` / `−0,60%`). На падающем рынке система автоматически облегчает шорты (`×0,90`) и ужесточает покупки (`×1,25`).
76
+ - **Глубина сопровождения `130 минут` (`holdBars = 13`) и фиксация прибыли:** первые 45 минут после открытия действует мягкий порог удержания (`0,35×`), затем к 130-й минуте он плавно поднимается к `0,95×` с подстройкой под сегодняшнюю волатильность бумаги. При достижении порога Take-Profit включается скользящая защита прибыли (в 1-м эшелоне удерживает `≥70%` пика, во 2-м эшелоне — `≥75%` пика с допустимым откатом не более `0,35%`).
75
77
 
76
78
  ---
77
79
 
@@ -89,30 +89,35 @@
89
89
 
90
90
  Даже идеальный прогноз (`IC > 0.10`) можно превратить в убыток, если неправильно входить, пересиживать сделки или торговать бумагами, в которых сломалась структура. Поэтому поверх прогноза работает **адаптивный движок портфеля**:
91
91
 
92
- ### 5.1. Ожидаемое движение (`Edge`) и Порог выгоды (`modelMinEdge`)
93
- Модель выдаёт прогноз в стандартных отклонениях $\hat{y}_i$. Чтобы перевести его в реальные ожидаемые проценты движения за горизонт $H$ свечей, вычисляется:
92
+ ### 5.1. Ожидаемое движение (`Edge`), эшелоны ликвидности и издержки «на сторону»
93
+ Модель выдаёт прогноз в стандартных отклонениях $\hat{y}_i$. Чтобы перевести его в реальные ожидаемые проценты движения за мульти-горизонт обучения $H = 15$ свечей ($2,5$ часа), вычисляется:
94
94
  $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
95
- Сделка открывается только тогда, когда ожидаемое движение перекрывает удвоенные издержки (вход + выход = $2 \times (\text{комиссия} + \text{проскальзывание})$) минимум в `modelMinEdge` раз (по умолчанию в **2.5 раза**).
95
+ Каждая сделка состоит из **двух операций (двух «сторон»)**: входа и выхода. При одинаковой комиссии брокера (`0,04%` на сторону = `0,08%` за круг) реальные потери в биржевом стакане зависят от эшелона ликвидности:
96
+ * **1-й эшелон (`TIER1_LIQUID_SYMBOLS`, 33 ликвидные акции Мосбиржи, включая `SBER`, `LKOH`, `GAZP`, `T`, `YDEX`, `PLZL`, `VTBR`, `OZON`, `MTLR`, `BSPB`, `RTKM`, `SVCB` и др.):** плотный стакан с потерями на спреде `~0,025%` → полные издержки **`0,065%` на сторону (`0,13%` за круг)**, базовый порог входа — **`1,0×`** (`~+0,40%`).
97
+ * **2–3-й эшелон (остальные ~92 акции, включая высоковолатильные бумаги с широким спредом `SMLT`, `PIKK`, `POSI`, `AFKS`, `VKCO`, `SPBE`, `UGLD`, `HEAD` и др.):** потери в стакане `~0,085%` → полные издержки **`0,125%` на сторону (`0,25%` за круг)**, порог входа повышен в **`1,20×` (`TIER2_EDGE_MULT`, `~+0,48%`)**.
96
98
 
97
- ### 5.2. Полушаговое сопровождение позиций (`H / 2`) и Двухступенчатый выход
98
- Вместо того чтобы смотреть на рынок только раз в `H` часов, в режиме самообучения портфель проверяется каждые **`H / 2` свечей** (при горизонте 2 часа — каждый час):
99
- * **В первой половине жизни сделки (`barsHeld < H`):** действует мягкий порог удержания (`0.35 × порог входа`), чтобы случайный внутридневной шум не выбил позицию сразу после открытия.
100
- * **По истечении горизонта (`barsHeld >= H`):** включается строгий порог удержания (`0.75 × порог входа`). Если сигнал сохраняет силу — позиция удерживается дальше без лишней перепродажи и двойной комиссии; если импульс затух — прибыль сразу фиксируется, а слот освобождается для новой идеи.
99
+ ### 5.2. Глубина сопровождения `130 минут` (`holdBars = 13`), динамическая рампа удержания и `Profit Lock`
100
+ В режиме самообучения портфель проверяется на каждом шаге `holdBars / 5` (каждые 20–30 минут), а новые внутридневные входы прекращаются за `130 минут` до окончания вечерней сессии (`21:30 МСК`):
101
+ * **Первые 45 минут жизни сделки:** действует мягкий порог удержания (`0,35 × порог входа`), чтобы случайный внутридневной шум не выбил позицию сразу после открытия.
102
+ * **С 45-й по 130-ю минуту (`exitHoldRatio`):** порог удержания плавно поднимается к `0,95 × порог входа`, динамически масштабируясь по отношению сегодняшней фактической волатильности бумаги к её обычной сигме (`realizedBarPct / sigmaBarPct` в коридоре `[0,75..1,25]`).
103
+ * **Скользящая фиксация прибыли (`profitLockLevels`):** включает защиту безубытка от `+0,85% × tierMult` (после 15 мин) и скользящий Take-Profit от `+1,20% × tierMult` (`+1,55% × tierMult` в первые 15 мин). В 1-м эшелоне трал удерживает не менее `70%` пиковой прибыли (допуская откат до `0,45%`), а во 2-м эшелоне действует уплотнённый трал — удерживает **не менее `75%` пика и допускает откат не более `0,35%`**, забирая импульс до разворота широкого спреда.
101
104
 
102
- ### 5.3. Временный карантин «акций-убийц» с экспоненциальным затуханием
105
+ ### 5.3. Временный карантин «акций-убийц» с экспоненциальным затуханием и пробоем сильным сигналом
103
106
  На рынке периодически возникают бумаги, в которых из-за корпоративного конфликта, допэмиссии или специфических новостей перестают работать статистические связи.
104
- * Система ведёт по каждой акции скользящий счётчик чистой доходности прошлых сделок `stockPnl[i]`.
105
- * Если бумага за серию сделок (`stockCnt >= 3`) принесла накопленный убыток ниже `-1.2%`, она автоматически ставится **на карантин** — новые сделки по ней блокируются.
106
- * **Важная механика амнистии:** на каждом шаге счётчики умножаются на коэффициент затухания `0.99` (период полураспада ~15 торговых дней). Поэтому бумага не банится навсегда — через 2–3 недели, когда аномалия на рынке проходит, акция возвращается в оборот.
107
+ * Система ведёт по каждой акции скользящий FIFO-счётчик чистой доходности закрытых кругов сделок `stockPnl[i]`.
108
+ * Если бумага за серию сделок (`stockCnt >= 2` при `stockPnl < -1.2%` или `stockPnl < -2.5%`) принесла накопленный убыток, она ставится **на карантин** — обычные сигналы по ней пропускаются.
109
+ * **Амнистия и пробой сильным сигналом (`110%`):** на каждом 30-минутном шаге счётчики умножаются на коэффициент затухания `0.9985` (период полураспада ~15 торговых дней), а особо сильный сигнал (**`≥ 110%` от индивидуального порога входа**) пробивает карантин досрочно.
107
110
 
108
111
  ### 5.4. Адаптивная балансировка `Long / Short` по тренду индекса Мосбиржи
109
112
  Модель предсказывает движение акции *относительно индекса*.
110
113
  * Когда индекс Мосбиржи находится в среднесрочном падающем тренде (ниже уровня 10 торговых дней назад или в 3-дневном проливе >1%), покупать акции против падающего рынка опасно, а продавать слабые акции (шорт) — наоборот, вдвойне выгодно.
111
114
  * В режиме `Покупки и продажи (longshort)` с включённым самообучением система автоматически **повышает входной фильтр для покупок (`×1.25`)** и **облегчает вход для коротких продаж (`×0.90`)** на падающем рынке (и наоборот — на растущем рынке).
112
- * Именно эта механика позволяет стратегии стабильно зарабатывать и в растущие месяцы, и на затяжном падении индекса Мосбиржи (например, +253.9% за период, где сам индекс упал на −21%).
115
+ * Именно эта механика позволяет стратегии стабильно зарабатывать и в растущие месяцы, и на затяжном падении индекса Мосбиржи.
113
116
 
114
- ### 5.5. Динамическое распределение капитала по слотам и силе сигнала (`Dynamic Top-K` + `Conviction Slot Sizing`)
115
- В режиме автопилота (`modelTopK = 0`) система не распыляет капитал на фиксированное число бумаг: если на рынке есть только 1 явный лидер, а остальные сигналы слабее 62–78% от лидера, капитал концентрируется в лучшем импульсе. При этом в режиме «Хозяин» (`owner`) слот масштабируется пропорционально силе сигнала (`Conviction Sizing`: вплоть до `1.65×` и до `75%` капитала в исключительных импульсах, на тарифе «VIP» — до `48%`, на «Профи» — до `28%`, на «Инвесторе» — до `24%`), с жёстким контролем, чтобы сумма всех открытых позиций никогда не превышала `99%` капитала (без кредитного плеча).
117
+ ### 5.5. Распределение капитала по эшелонам, плечу и чистой отдаче (`Dynamic Top-K` + `Conviction Sizing` + `netPickSort`)
118
+ В режиме автопилота (`modelTopK = 0`) система держит до 4 лонгов и до 4 шортов, распределяя капитал между отобранными лидерами пропорционально **чистой ожидаемой отдаче после вычета круговых издержек эшелона** (`|Edge| − 0,13%` для 1-го эшелона и `|Edge| − 0,25%` для 2-го):
119
+ * В **1-м эшелоне** размер слота масштабируется на полное плечо счёта (`maxLeverage`), а предел на одну бумагу составляет до **`50%` бюджета в лонг и до `38%` в шорт** (умноженные на плечо).
120
+ * Во **2-м эшелоне** действует асимметричное масштабирование от плеча счёта (`tierSizeMult`): для лонга — `max(0,50, 0,38 × плечо)`, для шорта — `max(0,35, 0,22 × плечо)`, при пределе `22%` в лонг / `18%` в шорт и жёстком потолке ликвидности **не более `15%` от медианного 10-минутного оборота свечи (`barTurnoverRub`)**.
116
121
 
117
122
  ---
118
123
 
@@ -150,7 +155,7 @@ $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
150
155
  4. **Только лимитные заявки с точным шагом цены (`limitOffsetPct`):** Заявки выставляются от живой цены с защитным лимитным отступом (по умолчанию `0.1%`), округлённым строго по шагу цены (`MINSTEP` / `minPriceIncrement`) конкретной бумаги.
151
156
  5. **Защита краёв сессии (`skipEdgeMinutes`):** В окна `06:50–07:25`, `09:50–10:10` и `18:35–18:55` МСК новые входы не открываются и фиксация прибыли не срабатывает на шуме стакана; аварийные стопы работают всегда.
152
157
  6. **Аварийный стоп позиции — 7% от средней цены входа** (у брокера стоп-заявкой и во внутреннем контуре). Модель торгует возврат к среднему, поэтому стоп стоит широко: проверка на истории показала, что прежние 4,5–5% закрывали позиции на пике отклонения и снижали итог на обеих половинах периода.
153
- 6.1. **Плечо (`maxLeverage`, настройка счёта):** множитель размера позиций в ликвидных бумагах 1-го эшелона и предел суммарной позиции (× капитал). Бумаги 2–3-го эшелона всегда торгуются половинным размером. Работает только при включённой маржинальной торговле; тариф «Инвестор» — без плеча, «Профи» — до 3, «VIP» и «Хозяин» — до 7. Предел заявки дополнительно проверяет брокер (`GetMaxLots`, маржинальный лимит).
158
+ 6.1. **Плечо (`maxLeverage`, настройка счёта):** множитель размера позиций и предел суммарной позиции (`×` капитал). В ликвидных бумагах 1-го эшелона размер масштабируется на полное плечо счёта, а во 2–3-м эшелоне действует асимметричное масштабирование (`max(0,50, 0,38 × плечо)` для лонга и `max(0,35, 0,22 × плечо)` для шорта) под защитой потолка `15%` от 10-минутного оборота свечи (`barTurnoverRub`). Работает только при включённой маржинальной торговле; тариф «Инвестор» — без плеча, «Профи» — до 3, «VIP» и «Хозяин» — до 7. Предел заявки дополнительно проверяет брокер (`GetMaxLots`, маржинальный лимит).
154
159
  6.2. **Стоп по общей просадке (`maxDrawdownPct`, настройка счёта, 0 — выкл.):** если оценка счёта опустилась от своего максимума на заданный процент, новые входы запрещаются до ручного снятия («Снять стоп по просадке»); открытые позиции сопровождаются. После вывода денег стоп снимается вручную.
155
160
  6.3. **Капитал в LQDT как залог** (при включённых «Свободные рубли в LQDT» и маржинальной торговле): паи LQDT не продаются под входы — внутридневные позиции открываются под обеспечение и закрываются до ночи (платы за перенос нет), а свободные рубли размещаются в LQDT, когда у стратегии нет открытых позиций.
156
161
  6.4. **Вход против планки:** если брокер отклонил вход из-за ценового коридора Мосбиржи (код 30099 — бумага у планки), вход в эту бумагу в ту же сторону блокируется до конца торгового дня.
@@ -166,13 +171,13 @@ $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
166
171
 
167
172
  Изменение правил проверяется в лаборатории `scripts/strategy-lab.mjs` (варианты — в `scripts/strategy-lab-variants.mjs`) и принимается, только если оно лучше текущих правил **и на первой, и на второй половине периода**. Каждая отправленная заявка записывается в журнал решений `trade_decisions` (причина, ожидание и порог, контекст дня, плечо, версия программы; `GET /api/brokers/:id/decisions`).
168
173
 
169
- Проверка правил 06.11.2026 (пошаговые прогнозы 06.2024–11.2026, 788 сделок, капитал 800 тыс. ₽, LQDT за период — 18,5% годовых):
174
+ Проверка правил 13.11.2026 (пошаговые прогнозы 06.2024–11.2026, 59 051 свеча, 900 сделок, капитал 800 тыс. ₽, LQDT за период — 18,5% годовых):
170
175
 
171
176
  | Правила | CAGR (весь период) | 1-я / 2-я половина | PF | Макс. просадка | Худшая сделка, % капитала |
172
177
  |---|---|---|---|---|---|
173
178
  | До 06.11.2026 (без LQDT, 2-й эшелон полным размером) | 12,9% | 19,3% / 6,8% | 1,40 | 4,4% | −2,0% |
174
- | Текущие, плечо 1 | 31,5% | 38,0% / 25,6% | 1,55 | 2,2% | −2,0% |
175
- | Текущие, плечо 3 | 52,1% | 56,2% / 48,5% | 1,87 | 7,0% | −6,8% |
176
- | Текущие, плечо 5 | 75,2% | 76,0% / 74,9% | 2,02 | 12,5% | −12,9% |
179
+ | Текущие (`holdBars=13`, ревизия эшелонов), плечо 1× | **39,5%** | **49,2% / 30,7%** | **1,85** | **2,0%** | −2,2% |
180
+ | Текущие (`holdBars=13`, ревизия эшелонов), плечо 2× | **59,8%** | **73,8% / 47,4%** | **2,03** | **3,6%** | −4,3% |
181
+ | Текущие (`holdBars=13`, ревизия эшелонов), плечо 3× | **83,7%** | **104,6% / 65,6%** | **2,07** | **5,9%** | −6,5% |
177
182
 
178
183
  Отклонены (хуже хотя бы на одной половине или с катастрофическим хвостом): защита вечным фьючерсом IMOEXF (издержки больше выигрыша), пассивные лимитные входы, снижение порога входа, 6–8 позиций на сторону, выход по времени, отключение фиксации прибыли, стоп по волатильности и «стоп отмены идеи», размер позиции по риску. Результат на истории не гарантирует будущий: плечо пропорционально увеличивает и доходность, и просадки.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuroalpha",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
package/public/app.js CHANGED
@@ -1651,6 +1651,20 @@ async function viewArchivedMarkdown(id) {
1651
1651
  }
1652
1652
  }
1653
1653
 
1654
+ async function startNewChatSession() {
1655
+ try {
1656
+ if (speechRec && speechActive) stopVoiceInput(false);
1657
+ await post('/api/chat/new');
1658
+ $('#archive-modal').hidden = true;
1659
+ if (document.body.classList.contains('chat-closed')) setChatClosed(false);
1660
+ $('#chat-input')?.focus();
1661
+ } catch (e) {
1662
+ toast(e.message, 'error');
1663
+ }
1664
+ }
1665
+
1666
+ $('#btn-chat-new')?.addEventListener('click', startNewChatSession);
1667
+ $('#archive-new-chat')?.addEventListener('click', startNewChatSession);
1654
1668
  $('#btn-chat-archive')?.addEventListener('click', openChatArchiveList);
1655
1669
  $('#archive-back')?.addEventListener('click', openChatArchiveList);
1656
1670
  $('#archive-close')?.addEventListener('click', () => { $('#archive-modal').hidden = true; });
package/public/index.html CHANGED
@@ -37,6 +37,9 @@
37
37
  <textarea id="chat-input" rows="2" placeholder="Спросите Стратега о прогнозе, портфеле или настройках счёта…"></textarea>
38
38
  <div class="prompt-bar">
39
39
  <div class="prompt-left">
40
+ <button type="button" id="btn-chat-new" class="ghost-select model-btn" title="Начать новую сессию со Стратегом (текущая беседа сохранится в архив)">
41
+ <span>➕ Новая</span>
42
+ </button>
40
43
  <button type="button" id="btn-chat-archive" class="ghost-select model-btn" title="Архив предыдущих сессий со Стратегом (.md)">
41
44
  <span>🗂 Архив</span>
42
45
  </button>
@@ -346,6 +349,7 @@
346
349
  <div class="modal-head" style="gap:10px;flex-wrap:wrap">
347
350
  <b id="archive-title">Архив сессий со Стратегом</b>
348
351
  <span class="grow"></span>
352
+ <button id="archive-new-chat" type="button" class="small" title="Начать новую сессию со Стратегом">➕ Новая сессия</button>
349
353
  <button id="archive-back" type="button" class="small" hidden>← Все сессии</button>
350
354
  <button id="archive-open-chat" type="button" class="primary small" hidden title="Загрузить эту сессию в панель чата и продолжить диалог">Открыть в чате</button>
351
355
  <button id="archive-download-md" type="button" class="small" hidden title="Скачать файл сессии в формате Markdown (.md)">⬇ Скачать .md</button>
@@ -13,7 +13,7 @@ import { barsFromHours, BAR_INTERVAL, BAR_MS } from '../model/dataset.js';
13
13
  import { forecastNow } from '../model/forecast.js';
14
14
  import { TIER1_LIQUID_SYMBOLS, QUARANTINE_OVERRIDE_RATIO, symbolEdgeMult, isStockQuarantined, computeAccountQuarantine, canEnterIntraday, shouldExitBeforeNight } from '../model/edge.js';
15
15
  import { liveDividendGuard } from '../model/dividends.js';
16
- import { STRATEGY_POLICY, sessionEdgeWarning, evaluateProfitLock, protectiveStopPct, exitHoldRatio, tierSizeMult, MAX_LEVERAGE } from '../model/policy.js';
16
+ import { STRATEGY_POLICY, sessionEdgeWarning, evaluateProfitLock, protectiveStopPct, exitHoldRatio, tierSizeMult, symbolCapRub, MAX_LEVERAGE } from '../model/policy.js';
17
17
  import { MODELS } from '../model/models.js';
18
18
  import { blockingNews } from '../news/index.js';
19
19
  import { round, MSK, DAY, fmtBarMsk } from '../util.js';
@@ -2398,7 +2398,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2398
2398
  const totalNew = newBuys.length + newShorts.length;
2399
2399
  const activeTotal = Math.max(2, activeLongs + activeShorts + totalNew);
2400
2400
  const dynSlotRub = (isOwnerPlan && autoAdapt)
2401
- ? budget * Math.min(0.45, Math.max(slotRub / Math.max(1, budget), 1.8 / Math.min(slots, activeTotal + 1)))
2401
+ ? budget * Math.min(0.50, Math.max(slotRub / Math.max(1, budget), 1.8 / Math.min(slots, activeTotal + 1)))
2402
2402
  : (vipBoost && autoAdapt)
2403
2403
  ? budget * Math.min(0.40, Math.max(slotRub / Math.max(1, budget), 1.5 / Math.min(slots, activeTotal + 1)))
2404
2404
  : slotRub;
@@ -2460,9 +2460,9 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2460
2460
  const lqdtLots = Math.floor(lqdtAlgoQty / lqdtSp.lotSize);
2461
2461
  const neededForNewRub = combinedNew.reduce((s, { r, side }) => {
2462
2462
  const isT1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
2463
- const rawCap = budget * (side === 'short' ? (isT1 ? 0.35 : 0.18) : (isT1 ? 0.45 : 0.22));
2463
+ const posSide = side === 'short' ? 'short' : 'long';
2464
2464
  const barTurnRub = !isT1 ? recentBarTurnoverRub(r.symbol) : 0;
2465
- const liqCap = (!isT1 && barTurnRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnRub * 0.12)) : rawCap;
2465
+ const liqCap = symbolCapRub(r.symbol, posSide, budget, barTurnRub);
2466
2466
  return s + Math.min(liqCap, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * convOf(r));
2467
2467
  }, 0);
2468
2468
  const needFromLqdtRub = Math.min(Math.max(0, budget * 0.99 - usedRub), neededForNewRub) - availRub;
@@ -2481,37 +2481,34 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2481
2481
  }
2482
2482
  }
2483
2483
  }
2484
+ const netPickWeight = (r) => {
2485
+ const rawPct = Math.abs(r.expectedPct || 0);
2486
+ if (!autoAdapt) return Math.max(0.01, rawPct);
2487
+ const roundTripCostPct = TIER1_LIQUID_SYMBOLS.has(r.symbol) ? 0.13 : 0.25;
2488
+ return Math.max(0.01, rawPct - roundTripCostPct);
2489
+ };
2484
2490
  if (autoAdapt && combinedNew.length > 1) {
2485
- combinedNew.sort((a, b) => Math.abs(b.r.expectedPct || 0) - Math.abs(a.r.expectedPct || 0));
2491
+ combinedNew.sort((a, b) => netPickWeight(b.r) - netPickWeight(a.r));
2486
2492
  }
2487
- let remWeightSum = combinedNew.reduce((s, x) => s + Math.max(0.01, Math.abs(x.r.expectedPct || 0)), 0);
2493
+ let remWeightSum = combinedNew.reduce((s, x) => s + netPickWeight(x.r), 0);
2488
2494
 
2489
- // Жёсткий предохранитель концентрации на одну бумагу в реальном стакане брокера:
2490
- // • для ликвидных бумаг 1-го эшелона: до 45% счёта на один лонг и до 35% счёта на один шорт;
2491
- // • для бумаг 2–3 эшелона (MRKC, MRKZ, NKHP, AQUA, FESH, FEES и др.): до 22% счёта на один лонг и до 18% на один шорт,
2495
+ // Жёсткий предохранитель концентрации на одну бумагу в реальном стакане брокера (symbolCapRub в policy.js):
2496
+ // • для ликвидных бумаг 1-го эшелона: до 50% счёта на один лонг и до 38% счёта на один шорт;
2497
+ // • для бумаг 2–3 эшелона: до 22% счёта на один лонг и до 18% на один шорт (но не более 15% от 10-минутного оборота свечи),
2492
2498
  // чтобы крупная заявка не выкупала тонкий стакан против самой себя и всегда оставляла капитал на 3–4 бумаги!
2493
- const maxLongTier1Rub = budget * 0.45;
2494
- const maxShortTier1Rub = budget * 0.35;
2495
- const maxLongTier2Rub = budget * 0.22;
2496
- const maxShortTier2Rub = budget * 0.18;
2497
-
2498
2499
  for (const { r, side } of combinedNew) {
2499
- const wCur = Math.max(0.01, Math.abs(r.expectedPct || 0));
2500
+ const wCur = netPickWeight(r);
2500
2501
  const propShare = autoAdapt && remWeightSum > 0 ? availRub * (wCur / remWeightSum) : availRub / Math.max(1, remNew);
2501
2502
  remWeightSum = Math.max(0, remWeightSum - wCur);
2502
2503
  remNew--;
2503
2504
  const isTier1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
2504
- // множитель размера по эшелону: 1-й эшелон — плечо счёта, 2–3-й — половинный размер (policy.js)
2505
- const tierMult = autoAdapt ? tierSizeMult(r.symbol, leverage) : 1;
2506
- const rawSideCapRub = tierMult * (side === 'short'
2507
- ? (isTier1 ? maxShortTier1Rub : maxShortTier2Rub)
2508
- : (isTier1 ? maxLongTier1Rub : maxLongTier2Rub));
2509
- // Лимит от реального 10-минутного оборота стакана (Liquidity Cap): во 2-м эшелоне не входим объёмом больше 12% от 10-минутного оборота биржи,
2505
+ const posSide = side === 'short' ? 'short' : 'long';
2506
+ // множитель размера по эшелону и направлению: 1-й эшелон — плечо счёта, 2–3-й — асимметричное масштабирование от плеча (policy.js)
2507
+ const tierMult = autoAdapt ? tierSizeMult(r.symbol, leverage, posSide) : 1;
2508
+ // Лимит от реального 10-минутного оборота стакана (Liquidity Cap): во 2-м эшелоне не входим объёмом больше 15% от 10-минутного оборота биржи,
2510
2509
  // чтобы заявка не становилась «стенкой» для стаканных роботов и исполнялась целиком без проскальзывания
2511
2510
  const barTurnoverRub = !isTier1 ? recentBarTurnoverRub(r.symbol) : 0;
2512
- const sideCapRub = (!isTier1 && barTurnoverRub > 0)
2513
- ? Math.max(45000, Math.min(rawSideCapRub, barTurnoverRub * 0.12))
2514
- : rawSideCapRub;
2511
+ const sideCapRub = symbolCapRub(r.symbol, posSide, budget * tierMult, barTurnoverRub);
2515
2512
  const rawSum = Math.min(sideCapRub, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * tierMult * convOf(r));
2516
2513
  const sum = Math.min(rawSum, propShare, availRub);
2517
2514
  const refPx = r.price || specs[r.symbol]?.lastPrice || 0;
@@ -8,12 +8,13 @@
8
8
  export const ENTRY_EDGE_MULT = 1.60;
9
9
  export const TIER2_EDGE_MULT = 1.20;
10
10
 
11
- // Голубые фишки и наиболее ликвидные акции 1-го эшелона Мосбиржи (глубокий стакан, минимальный спред):
12
- // высоковолатильные бумаги со спредом 2-го эшелона (SMLT, PIKK, POSI, AFKS, VKCO, CBOM, BSPB, SVCB) идут по порогу TIER2_EDGE_MULT.
11
+ // Голубые фишки и наиболее ликвидные акции 1-го эшелона Мосбиржи (глубокий стакан, минимальный спред, нулевая частота срабатывания 7%-го стопа):
12
+ // бумаги с повышенным хвостовым риском и широким спредом (SMLT, PIKK, POSI, AFKS, VKCO, SPBE, UGLD, HEAD, CBOM) идут по правилам 2-го эшелона (TIER2_EDGE_MULT).
13
13
  export const TIER1_LIQUID_SYMBOLS = new Set([
14
14
  'SBER', 'SBERP', 'LKOH', 'GAZP', 'T', 'YDEX', 'ROSN', 'NVTK', 'GMKN', 'PLZL',
15
15
  'VTBR', 'TATN', 'TATNP', 'SNGS', 'SNGSP', 'ALRS', 'CHMF', 'NLMK', 'MAGN', 'MOEX',
16
- 'OZON', 'HEAD', 'PHOR', 'AFLT', 'MTSS', 'IRAO', 'TRNFP', 'RUAL', 'FLOT', 'X5',
16
+ 'OZON', 'PHOR', 'AFLT', 'MTSS', 'IRAO', 'TRNFP', 'RUAL', 'FLOT', 'X5',
17
+ 'MTLR', 'BSPB', 'RTKM', 'SVCB',
17
18
  ]);
18
19
 
19
20
  export function symbolEdgeMult(symbol, { sigma = 0, avgSigma = 0, avgBarValRub = 0 } = {}) {
@@ -19,11 +19,28 @@ 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
+ // Множитель размера позиции по эшелонам:
23
+ // • 1-й эшелон (33 ликвидные акции, PF 2,86 и 0 стопов на 59 051 свече): масштабируется на полное плечо счёта (lev),
24
+ // предельный вес бумаги — до 50% бюджета в лонг и до 38% в шорт (базовый слот 0,50);
25
+ // • 2-й эшелон: асимметричное масштабирование от плеча счёта (лонг max(0,50, 0,38 × lev), шорт max(0,35, 0,22 × lev))
26
+ // под защитой потолка реальной ликвидности стакана (15% от медианного 10-минутного оборота свечи barTurnoverRub)
27
+ // и плотного сопровождения Take-Profit (удержание ≥75% пика и откат ≤0,35%).
28
+ // Проверка 13.11.2026 (06.2024–11.2026, 59 051 свеча): улучшает итог на обеих половинах истории при любом плече
29
+ // • плечо 1×: CAGR 49,2%/30,7% по половинам (весь 39,5%, PF 1,85, DD 2,0%) против 43,8%/27,3% (весь 35,1%, PF 1,63);
30
+ // • плечо 2×: CAGR 73,8%/47,4% по половинам (весь 59,8%, 6 мес. 105,0%, PF 2,03, DD 3,6%) против 54,5%/40,6% (весь 47,2%, PF 1,87, DD 3,4%);
31
+ // • плечо 3×: CAGR 104,6%/65,6% по половинам (весь 83,7%, PF 2,07, DD 5,9%) против 65,9%/55,2% (весь 60,3%, PF 2,03, DD 6,0%).
32
+ sizeMultT2LongBase: 0.50,
33
+ sizeMultT2LongLev: 0.38,
34
+ sizeMultT2ShortBase: 0.35,
35
+ sizeMultT2ShortLev: 0.22,
36
+ capT1Long: 0.50,
37
+ capT1Short: 0.38,
38
+ capT2Long: 0.22,
39
+ capT2Short: 0.18,
40
+ turnoverCapMult: 0.15,
41
+ tpRetainT2: 0.75,
42
+ tpTrailT2: 0.35,
43
+ netPickSort: true,
27
44
  // капитал хранится в фонде денежного рынка LQDT как залог: внутридневные позиции открываются под обеспечение
28
45
  // (без платы за перенос — позиции закрываются до ночи), LQDT не продаётся под входы
29
46
  cashYield: true,
@@ -63,12 +80,15 @@ export function sessionEdgeWarning(ts = Date.now()) {
63
80
  // ---------- фиксация прибыли ----------
64
81
  // Уровни в процентах выигрыша от средней цены. peakGainPct — лучший выигрыш с момента входа.
65
82
  // Возвращает { floorPct, minExitPct, kind } — выход, когда текущий выигрыш опустился до floorPct, но не ниже minExitPct; или null.
66
- export function profitLockLevels(sym, peakGainPct, heldMin, roundTripFeePct = 0.10) {
83
+ export function profitLockLevels(sym, peakGainPct, heldMin, roundTripFeePct = 0.10, policy = STRATEGY_POLICY) {
84
+ const isT1 = TIER1_LIQUID_SYMBOLS.has(sym);
67
85
  const tierMult = symbolEdgeMult(sym);
68
86
  const minNetFloorPct = Math.max(roundTripFeePct + 0.30, Math.round(0.38 * tierMult * 100) / 100);
69
87
  const tpTriggerPct = (heldMin < 15 ? 1.55 : 1.20) * tierMult;
70
88
  if (peakGainPct >= tpTriggerPct) {
71
- return { floorPct: Math.max(peakGainPct * 0.70, peakGainPct - 0.45, minNetFloorPct), minExitPct: minNetFloorPct, kind: 'tp' };
89
+ const retain = !isT1 && policy?.tpRetainT2 ? policy.tpRetainT2 : 0.70;
90
+ const trail = !isT1 && policy?.tpTrailT2 ? policy.tpTrailT2 : 0.45;
91
+ return { floorPct: Math.max(peakGainPct * retain, peakGainPct - trail, minNetFloorPct), minExitPct: minNetFloorPct, kind: 'tp' };
72
92
  }
73
93
  if (heldMin >= 15 && peakGainPct >= 0.85 * tierMult) {
74
94
  return { floorPct: Math.max(minNetFloorPct, peakGainPct * 0.45), minExitPct: roundTripFeePct + 0.22, kind: 'be' };
@@ -123,23 +143,36 @@ export function exitHoldRatio(heldMin, horizonMin, { sigmaBarPct = 0, realizedBa
123
143
 
124
144
  // ---------- плечо и множители размера по эшелонам ----------
125
145
  export const MAX_LEVERAGE = 7;
126
- export function tierSizeMult(symbol, leverage = 1, policy = STRATEGY_POLICY) {
146
+ export function tierSizeMult(symbol, leverage = 1, sideOrPolicy = 'long', policyArg = STRATEGY_POLICY) {
147
+ const isObj = sideOrPolicy && typeof sideOrPolicy === 'object';
148
+ const side = isObj ? 'long' : (sideOrPolicy === 'short' ? 'short' : 'long');
149
+ const policy = isObj ? sideOrPolicy : (policyArg || STRATEGY_POLICY);
127
150
  const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
128
- return TIER1_LIQUID_SYMBOLS.has(symbol) ? lev : (policy.sizeMultT2 ?? 1);
151
+ if (TIER1_LIQUID_SYMBOLS.has(symbol)) return policy.sizeMultT1 ?? lev;
152
+ if (policy.sizeMultT2 !== undefined) return policy.sizeMultT2;
153
+ const raw = side === 'short'
154
+ ? Math.max(policy.sizeMultT2ShortBase ?? 0.35, (policy.sizeMultT2ShortLev ?? 0.22) * lev)
155
+ : Math.max(policy.sizeMultT2LongBase ?? 0.50, (policy.sizeMultT2LongLev ?? 0.38) * lev);
156
+ return Math.round(raw * 1000) / 1000;
129
157
  }
130
158
 
131
159
  // ---------- предельный размер позиции на одну бумагу ----------
132
- export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0) {
160
+ export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY) {
133
161
  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;
162
+ const isShort = side === 'short';
163
+ const capPct = isShort
164
+ ? (isT1 ? (policy?.capT1Short ?? 0.38) : (policy?.capT2Short ?? 0.18))
165
+ : (isT1 ? (policy?.capT1Long ?? 0.50) : (policy?.capT2Long ?? 0.22));
166
+ const rawCap = budget * capPct;
167
+ const turnMult = policy?.turnoverCapMult ?? 0.15;
168
+ return (!isT1 && barTurnoverRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnoverRub * turnMult)) : rawCap;
136
169
  }
137
170
 
138
171
  // Размер новой позиции в рублях до учёта свободного кэша и пропорциональной доли:
139
172
  // 'slot' — рабочий слот × сила сигнала (conv), не больше предела бумаги;
140
173
  // 'risk' — при срабатывании стопа счёт теряет не больше riskPct% бюджета.
141
174
  export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0 }, policy = STRATEGY_POLICY) {
142
- const cap = symbolCapRub(symbol, side, budget, barTurnoverRub);
175
+ const cap = symbolCapRub(symbol, side, budget, barTurnoverRub, policy);
143
176
  const sz = policy.sizing || {};
144
177
  if (sz.mode === 'risk' && stopPct > 0) {
145
178
  const byRisk = (budget * (sz.riskPct / 100)) / (stopPct / 100);
@@ -12,6 +12,7 @@ import { strategyDefaults } from '../config.js';
12
12
  import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isRegimeShocked, isMarketDown } from './edge.js';
13
13
  import { buildDividendCalendar, nextDividendAt, dividendRules, DIV_TAX } from './dividends.js';
14
14
  import { simulateStrategy, alignSeries } from './simulator.js';
15
+ import { STRATEGY_POLICY, exitHoldRatio } from './policy.js';
15
16
  import { ensureLqdtHistory } from '../data/lqdt.js';
16
17
 
17
18
  const DAY = 86400000;
@@ -38,7 +39,9 @@ export { isMarketDown };
38
39
  // 15: защита от биржевых планок (high === low) и блокировка утренних шортов 2-го эшелона в первые 20 минут сессии (07:00–07:20 МСК)
39
40
  // 16: главная оценка — симулятор с правилами живого контура (simulator.js), аварийный стоп 7%
40
41
  // 17: капитал в LQDT как залог, половинный размер 2-го эшелона, строки с плечом 3 и 5
41
- const DATA_VERSION = 17;
42
+ // 18: глубина сопровождения позиции и вечернего окна входов holdBars=13 (130 мин) во всех контурах симуляции
43
+ // 19: ревизия правил 1-го и 2-го эшелонов (MTLR/BSPB/RTKM/SVCB в T1, асимметричный масштаб T2 от плеча, лимит 50%/38% T1 и плотный трал T2)
44
+ const DATA_VERSION = 19;
42
45
 
43
46
  const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
44
47
  function reuseParamsKey(params) {
@@ -395,7 +398,9 @@ async function run(runId, p, state) {
395
398
  const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
396
399
  const stockAllowed = (i, edgeRatio = 0) => !autoAdapt || !isStockQuarantined(stockCnt[i], stockPnl[i], edgeRatio);
397
400
  let curExitRatio = autoAdapt ? 0.95 : exitRatio;
398
- const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
401
+ const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
402
+ const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
403
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
399
404
  const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
400
405
  let closedThisBar = new Set();
401
406
 
@@ -458,21 +463,21 @@ async function run(runId, p, state) {
458
463
  }
459
464
  }
460
465
  }
461
- const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
466
+ const timeOk = !autoAdapt || canEnterIntraday(barTs, holdBars, BAR_INTERVAL);
462
467
  const canOpenAt = (i, edgeRatio = 0) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i, edgeRatio) && canEnter(i, t);
463
468
  const okLong = (i) => !divRule(i, t)?.blockLong && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
464
- 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') && canEnterIntraday(barTs, H, BAR_INTERVAL, 'short', tokens[i].symbol)));
469
+ 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') && canEnterIntraday(barTs, holdBars, BAR_INTERVAL, 'short', tokens[i].symbol)));
465
470
  let order, entries;
466
471
  if (dir !== 'long') {
467
472
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
468
473
  const need = effMinEdge * 2 * realCost * needMult;
469
474
  const exitNeed = need * curExitRatio;
470
- const earlyExitNeed = need * 0.35;
471
- // держим позицию, пока ожидаемое движение выше порога выхода (первые два полушага — мягкий порог, чтобы не резать сделку от шума)
475
+ // держим позицию по единой рампе удержания exitHoldRatio (до 45 мин — 0.35, затем плавный подъём к 0.95 к концу глубины сопровождения horizonMin)
472
476
  for (const [i, h] of [...held]) {
473
477
  const e = edge(i);
474
478
  const barsHeld = tIn - h.entryT;
475
- const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
479
+ const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
480
+ const req = need * holdRatio * symMult[i];
476
481
  const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
477
482
  if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) {
478
483
  closePos(i, h, tIn, false, shocked ? 'Защита от экстремального сквиза/обвала' : 'Прогноз ослаб');
@@ -513,11 +518,11 @@ async function run(runId, p, state) {
513
518
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
514
519
  const need = effMinEdge * 2 * realCost * needMult;
515
520
  const exitNeed = need * curExitRatio;
516
- const earlyExitNeed = need * 0.35;
517
521
  for (const [i, h] of [...held]) {
518
522
  const e = edge(i);
519
523
  const barsHeld = tIn - h.entryT;
520
- const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
524
+ const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
525
+ const req = need * holdRatio * symMult[i];
521
526
  const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
522
527
  if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn, false, shocked ? 'Защита от экстремального обвала' : '');
523
528
  }
@@ -1120,7 +1125,9 @@ async function runStabilityLoop(targetId, runRow, st) {
1120
1125
  const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
1121
1126
  const stockAllowed = (i, edgeRatio = 0) => !autoAdapt || !isStockQuarantined(stockCnt[i], stockPnl[i], edgeRatio);
1122
1127
  const curExitRatio = autoAdapt ? 0.95 : exitRatio;
1123
- const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
1128
+ const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
1129
+ const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
1130
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
1124
1131
  const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
1125
1132
  let closedThisBar = new Set();
1126
1133
  const closePos = (i, h, t) => {
@@ -1158,17 +1165,18 @@ async function runStabilityLoop(targetId, runRow, st) {
1158
1165
  if (shouldExitBeforeNight(barTs, tsOf(h.entryT), stepBars, BAR_INTERVAL)) closePos(i, h, tIn);
1159
1166
  }
1160
1167
  }
1161
- const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
1168
+ const timeOk = !autoAdapt || canEnterIntraday(barTs, holdBars, BAR_INTERVAL);
1162
1169
  const canOpenAt = (i, edgeRatio = 0) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i, edgeRatio) && canEnter(i, t);
1163
1170
  const okLong = (i) => !divRule(i, t)?.blockLong && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
1164
- const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || (!isRegimeShocked(ds, i, t, 'short') && canEnterIntraday(barTs, H, BAR_INTERVAL, 'short', tokens[i].symbol)));
1171
+ const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || (!isRegimeShocked(ds, i, t, 'short') && canEnterIntraday(barTs, holdBars, BAR_INTERVAL, 'short', tokens[i].symbol)));
1165
1172
  const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
1166
- const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio, earlyExitNeed = need * 0.35;
1173
+ const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio;
1167
1174
  let order, entries;
1168
1175
  if (dir !== 'long') {
1169
1176
  for (const [i, h] of [...held]) {
1170
1177
  const e = edge(i), barsHeld = tIn - h.entryT;
1171
- const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
1178
+ const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
1179
+ const req = need * holdRatio * symMult[i];
1172
1180
  const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
1173
1181
  if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
1174
1182
  }
@@ -1196,7 +1204,8 @@ async function runStabilityLoop(targetId, runRow, st) {
1196
1204
  } else if (effMinEdge > 0) {
1197
1205
  for (const [i, h] of [...held]) {
1198
1206
  const e = edge(i), barsHeld = tIn - h.entryT;
1199
- const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
1207
+ const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
1208
+ const req = need * holdRatio * symMult[i];
1200
1209
  const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
1201
1210
  if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn);
1202
1211
  }
@@ -17,7 +17,7 @@ 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 { STRATEGY_POLICY, MAX_LEVERAGE, sessionEdgeWarning, profitLockLevels, protectiveStopPct, exitHoldRatio, tierSizeMult, targetPositionRub, counterTrendBlocked, thesisStopPx, dayContext } from './policy.js';
21
21
 
22
22
  // Выравнивание внешнего ряда (свечи IMOEXF, дневные цены LQDT) по шкале датасета с переносом последней цены
23
23
  export function alignSeries(timeline, rows) {
@@ -50,7 +50,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
50
50
  const cooldownBars = Math.round((policy.cooldownMin ?? 30) / BAR_INTERVAL);
51
51
  const stopCooldownBars = Math.round((policy.stopCooldownMin ?? 60) / BAR_INTERVAL);
52
52
  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));
53
+ const 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));
54
54
  const convMax = plan === 'owner' ? 1.65 : plan === 'vip' ? 1.30 : 1;
55
55
  const convK = plan === 'owner' ? 0.30 : 0.25;
56
56
  const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
@@ -189,7 +189,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
189
189
  }
190
190
  if (done) continue;
191
191
  if (policy.profitLock) {
192
- const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart, roundTripFeePct);
192
+ const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart, roundTripFeePct, policy);
193
193
  if (lv) {
194
194
  const gO = gainOf(pos, o) * 100;
195
195
  if (gO <= lv.floorPct && gO >= lv.minExitPct) { closePos(i, pos, t, o, 'Фиксация прибыли', 'lock'); stats.locks++; done = true; }
@@ -199,7 +199,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
199
199
  if (done) continue;
200
200
  pos.peakGain = Math.max(pos.peakGain, gainOf(pos, favorable));
201
201
  if (policy.profitLock) {
202
- const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart + BAR_INTERVAL, roundTripFeePct);
202
+ const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart + BAR_INTERVAL, roundTripFeePct, policy);
203
203
  const gC = gainOf(pos, c) * 100;
204
204
  if (lv && gC <= lv.floorPct && gC >= lv.minExitPct) { closePos(i, pos, t, c, 'Фиксация прибыли', 'lock'); stats.locks++; }
205
205
  }
@@ -325,12 +325,13 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
325
325
  ...candL.slice(0, Math.max(0, K - nL)).map((i) => [i, 'long']),
326
326
  ...candS.slice(0, Math.max(0, K - nS)).map((i) => [i, 'short']),
327
327
  ];
328
- picks.sort((a, b) => Math.abs(edge(b[0])) - Math.abs(edge(a[0])));
328
+ 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))));
329
+ picks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
329
330
  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);
331
+ let remW = picks.reduce((s, [i]) => s + pickWeight(i), 0);
331
332
  for (const [i, side] of picks) {
332
333
  const e = edge(i);
333
- const w = Math.max(0.0001, Math.abs(e));
334
+ const w = pickWeight(i);
334
335
  const propShare = remW > 0 ? avail * (w / remW) : avail;
335
336
  remW -= w;
336
337
  const sigmaBarPct = sg[i] * 100;
@@ -338,7 +339,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
338
339
  if (counterTrendBlocked(side, ctx, policy)) { stats.blockedCT++; continue; }
339
340
  const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
340
341
  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
+ const tm = tierSizeMult(tokens[i].symbol, lev, side, policy);
342
343
  if (!(tm > 0)) continue;
343
344
  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
345
  const alloc = Math.min(target, propShare, avail);
@@ -249,16 +249,30 @@ ${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
+ - **Dynamic Top-K и Conviction Sizing:** робот держит до 4 лонгов и до 4 шортов. Базовый слот в режиме владельца — 50% бюджета, масштабируется на силу сигнала conv (от 0,85× до 1,65×).
265
+ - **Масштабирование по эшелонам и плечу (tierSizeMult и symbolCapRub):**
266
+ 1. В **1-м эшелоне** размер позиции умножается на полное плечо счёта (maxLeverage = 1×..7×), а потолок концентрации на одну бумагу составляет **50% в лонг и 38% в шорт** (умноженные на плечо).
267
+ 2. Во **2-м эшелоне** действует асимметричное масштабирование от плеча счёта: для **лонга** множитель равен max(0,50, 0,38 × плечо) (на плече 2× → 0,76×, на 3× → 1,14×), а для более рискованного **шорта** — max(0,35, 0,22 × плечо) (на 1× → 0,35×, на 2× → 0,44×, на 3× → 0,66×). Предел на одну бумагу 2-го эшелона — 22% в лонг и 18% в шорт, но **не более 15% от реального медианного 10-минутного оборота стакана (barTurnoverRub)**, чтобы заявка никогда не двигала цену против себя.
268
+ - **Приоритет распределения капитала (netPickSort):** кандидаты в слоты отбираются по силе z-скора прогноза, а очерёдность и доля капитала между отобранными сделками определяются по **чистой ожидаемой отдаче за вычетом круговых издержек эшелона** (|expectedPct| − 0,13% для 1-го эшелона и |expectedPct| − 0,25% для 2-го эшелона).
269
+ - **Капитал в фонде денежного рынка LQDT как залог:** свободный капитал работает в LQDT, а внутридневные позиции открываются под обеспечение и закрываются до ночи (без платы брокеру за перенос через ночь).
270
+ - **Глубина сопровождения позиций (130 минут), фиксация прибыли и защитные контуры:**
271
+ - **Глубина сопровождения (holdBars = 13 свечей = 130 минут = 2 ч 10 м):** плановая ревизия портфеля идёт каждые 20–30 минут (holdBars / 5), а вечернее окно новых внутридневных входов закрывается за 130 минут до конца торгов (после **21:30 МСК** новые входы не открываются, перед 23:40–23:50 МСК позиции закрываются перед ночью).
272
+ - **Плавная рампа удержания (exitHoldRatio):** первые **45 минут** после входа действует мягкий порог удержания (0,35× от порога входа), чтобы сделку не выбило шумом; затем с 45-й по **130-ю минуту** порог плавно поднимается к 0,95×, динамически подстраиваясь под сегодняшнюю волатильность бумаги (realizedBarPct / sigmaBarPct в коридоре [0,75..1,25]: в спокойный день держит дольше, в турбулентный фиксирует быстрее).
273
+ - **Скользящая фиксация прибыли (profitLockLevels):** включает безубыток от +0,85% × tierMult (после 15 мин) и скользящий Take-Profit от +1,20% × tierMult (+1,55% × tierMult в первые 15 мин). В 1-м эшелоне трал удерживает ≥70% пиковой прибыли (допуская откат до 0,45%), а во 2-м эшелоне работает **уплотнённый трал — удерживает ≥75% пика и допускает откат не более 0,35%**, забирая импульс до разворота спреда.
274
+ - **Аварийный стоп — 7% от средней цены входа** (выставляется реальной стоп-заявкой у брокера и контролируется внутри свечи).
275
+ - **Карантин убыточных бумаг (isStockQuarantined):** FIFO-учёт кругов сделок с периодом полураспада ~15 торговых дней (0,9985 на 30-минутном шаге); если бумага накопила серию убытков (≥2 сделок с суммарным итогом < −1,2% или < −2,5%), слабые сигналы по ней пропускаются, но **сильный сигнал (≥ 110% от индивидуального порога входа) пробивает карантин**.
262
276
 
263
277
  ## Брокерские счета и режимы работы
264
278
  Полная поддержка — Т-Банк (T-Invest API, включая секцию TQBR и SPBRU для SPBE). Финам, БКС, Алор, Цифра брокер и ITI Capital — бета, только «Подтверждение заявок». СберИнвестиции, ВТБ и Альфа-Инвестиции сейчас не подключаются (нет открытого API для заявок).