neuroalpha 0.6.11 → 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.
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +5 -4
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +12 -9
- package/package.json +1 -1
- package/server/brokers/index.js +179 -144
- package/server/model/policy.js +173 -35
- package/server/model/research.js +4 -3
- package/server/model/simulator.js +148 -19
- package/server/strategist/index.js +7 -6
package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md
CHANGED
|
@@ -69,9 +69,10 @@
|
|
|
69
69
|
|
|
70
70
|
В разделе **«Прогноз сейчас»** каждые **45 секунд** по живым котировкам Мосбиржи обновляются две таблицы: **«Сильнее рынка»** (кандидаты в покупку) и **«Слабее рынка»** (кандидаты в шорт).
|
|
71
71
|
|
|
72
|
-
- **Разделение на 1-й и 2-й эшелоны
|
|
73
|
-
- **1-й эшелон (33 наиболее ликвидные акции Мосбиржи):** низкие потери в стакане (`0,065%` на сторону = `0,13%` за полный круг со всеми комиссиями), базовый порог входа `~+0,40%`,
|
|
74
|
-
- **2–3-й эшелон (остальные ~92 акции):** более широкий спред в стакане (`0,125%` на сторону = `0,25%` за полный круг), порог входа повышен в `1,20×` (`~+0,48%`),
|
|
72
|
+
- **Разделение на 1-й и 2-й эшелоны ликвидности, перелив капитала и потеснение:**
|
|
73
|
+
- **1-й эшелон (33 наиболее ликвидные акции Мосбиржи):** низкие потери в стакане (`0,065%` на сторону = `0,13%` за полный круг со всеми комиссиями), базовый порог входа `~+0,40%`, живой `Conviction Sizing` разгоняет сильные одиночные сигналы вплоть до `92%` бюджета в лонг и `70%` в шорт на `1×` (в режиме «Хозяин»; до `75% / 56%` на «VIP», `52% / 40%` на «Профи», `58%` на «Инвесторе»), а с ростом плеча счёта лимит одной бумаги растёт сублинейно (`1 + 0,80 × (плечо − 1)`), сохраняя свободный маржинальный резерв под параллельные идеи.
|
|
74
|
+
- **2–3-й эшелон (остальные ~92 акции):** более широкий спред в стакане (`0,125%` на сторону = `0,25%` за полный круг), порог входа повышен в `1,20×` (`~+0,48%`), прямой предел позиции до `17%` в лонг и до `7%` в шорт на `1×` (с плавным ростом по плечу `0,72 / 0,40`) и жёстким потолком не более `15%` от реального 10-минутного оборота свечи.
|
|
75
|
+
- **Итеративный перелив (`water-filling`) и динамическое потеснение:** мелкие по ёмкости сделки 2-го эшелона не отнимают квоту у лидеров 1-го эшелона, а при нехватке лимита под новый сильный сигнал 1-го эшелона робот освобождает место ротацией остывшей позиции или частичным подрезанием 2-го эшелона (на `auto` — в реальном времени, на `confirm` — в плановых 30-минутных пакетах).
|
|
75
76
|
- **Авто-балансировка по тренду индекса:** когда индекс Мосбиржи растёт, покупки идут по базовому порогу (`+0,40%` / `+0,48%`), а вход в контртрендовый шорт ужесточается в `1,25×` (`−0,50%` / `−0,60%`). На падающем рынке система автоматически облегчает шорты (`×0,90`) и ужесточает покупки (`×1,25`).
|
|
76
77
|
- **Глубина сопровождения `130 минут` (`holdBars = 13`) и фиксация прибыли:** первые 45 минут после открытия действует мягкий порог удержания (`0,35×`), затем к 130-й минуте он плавно поднимается к `0,95×` с подстройкой под сегодняшнюю волатильность бумаги. При достижении порога Take-Profit включается скользящая защита прибыли (в 1-м эшелоне удерживает `≥70%` пика, во 2-м эшелоне — `≥75%` пика с допустимым откатом не более `0,35%`).
|
|
77
78
|
|
|
@@ -91,5 +92,5 @@
|
|
|
91
92
|
- Торговля **в рост и в падение (с шортами)** с повышенной концентрацией слота;
|
|
92
93
|
- Количество брокерских счетов и начальная сумма торговли определяются **по индивидуальному согласованию**.
|
|
93
94
|
- **Режим «Хозяин» (`owner` — для владельца / администратора платформы):**
|
|
94
|
-
-
|
|
95
|
+
- **Максимальная отдача капитала** (внутричасовой перехват `H/5` каждые 30 минут, живой `Conviction Sizing` до `92%` капитала в лонг / `70%` в шорт в 1-м эшелоне с сублинейным плечом и динамическим потеснением), без ограничений по числу счетов и сумме капитала;
|
|
95
96
|
- Эксклюзивный доступ к разделу **«Эксперименты»**.
|
|
@@ -114,10 +114,12 @@ $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
|
|
|
114
114
|
* В режиме `Покупки и продажи (longshort)` с включённым самообучением система автоматически **повышает входной фильтр для покупок (`×1.25`)** и **облегчает вход для коротких продаж (`×0.90`)** на падающем рынке (и наоборот — на растущем рынке).
|
|
115
115
|
* Именно эта механика позволяет стратегии стабильно зарабатывать и в растущие месяцы, и на затяжном падении индекса Мосбиржи.
|
|
116
116
|
|
|
117
|
-
### 5.5.
|
|
118
|
-
|
|
119
|
-
*
|
|
120
|
-
*
|
|
117
|
+
### 5.5. Адаптивная загрузка без холостого резерва: живой `Conviction Sizing`, сублинейное плечо, `water-filling` и двухступенчатое потеснение
|
|
118
|
+
Поскольку `81,5%` активного времени на рынке открыта всего **одна** позиция, система не замораживает половину лимита в «мёртвом резерве» под гипотетические будущие сделки, а решает три задачи раздельно:
|
|
119
|
+
* **1-й эшелон (живой `Conviction Sizing` + сублинейное плечо):** пограничный сигнал получает умеренный базовый объём, а сильный одиночный импульс пропорционально `conv` (до `1,92×` в режиме «Хозяин») разгоняется до **`92%` бюджета в лонг и `70%` в шорт** на счёте `1×` (`75% / 56%` на «VIP», `52% / 40%` на «Профи», `58%` в лонг на «Инвесторе»). С ростом плеча счёта потолок *одной* бумаги растёт **сублинейно** (`1 + 0,80 × (плечо − 1)`), удерживая риск одиночной сделки в рамках контроля просадки и превращая остаток бесплатного внутридневного плеча под залог `LQDT` в эластичный резерв под 2-ю и 3-ю параллельные идеи.
|
|
120
|
+
* **2-й эшелон (прямой риск-лимит + стакан биржи):** без двойного перемножения штрафов действует прямой потолок — до **`17%` бюджета в лонг** (`1 + 0,72 × (плечо − 1)`) и до **`7%` в шорт** (`1 + 0,40 × (плечо − 1)`), жёстко отсечённый по реальной глубине биржевого стакана (**не более `15%` от медианного 10-минутного оборота свечи `barTurnoverRub`**).
|
|
121
|
+
* **Итеративный перелив (`waterFillAllocations`) и чистая отдача (`netPickSort`):** веса сделок рассчитываются за вычетом круговых издержек эшелона (`|Edge| − 0,13%` в 1-м эшелоне и `|Edge| − 0,25%` во 2-м). При одновременном входе сделки с узким лимитом (2-й эшелон / тонкий стакан) забирают ровно свой потолок, а весь оставшийся ресурс переливается крупным лидерам 1-го эшелона без пустых остатков.
|
|
122
|
+
* **Двухступенчатое динамическое потеснение (`crowdOut`):** если свободного лимита реально не хватает под новый сильный сигнал 1-го эшелона, робот (с защитной выдержкой `≥ 20 минут` от «пилы») либо ротирует остывшую позицию целиком, либо частично подрезает позицию 2-го эшелона с полным сохранением `entryTs` и накопленного `peakPrice` для скользящего `Profit Lock`.
|
|
121
123
|
|
|
122
124
|
---
|
|
123
125
|
|
|
@@ -155,7 +157,7 @@ $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
|
|
|
155
157
|
4. **Только лимитные заявки с точным шагом цены (`limitOffsetPct`):** Заявки выставляются от живой цены с защитным лимитным отступом (по умолчанию `0.1%`), округлённым строго по шагу цены (`MINSTEP` / `minPriceIncrement`) конкретной бумаги.
|
|
156
158
|
5. **Защита краёв сессии (`skipEdgeMinutes`):** В окна `06:50–07:25`, `09:50–10:10` и `18:35–18:55` МСК новые входы не открываются и фиксация прибыли не срабатывает на шуме стакана; аварийные стопы работают всегда.
|
|
157
159
|
6. **Аварийный стоп позиции — 7% от средней цены входа** (у брокера стоп-заявкой и во внутреннем контуре). Модель торгует возврат к среднему, поэтому стоп стоит широко: проверка на истории показала, что прежние 4,5–5% закрывали позиции на пике отклонения и снижали итог на обеих половинах периода.
|
|
158
|
-
6.1. **Плечо (`maxLeverage`, настройка счёта):**
|
|
160
|
+
6.1. **Плечо (`maxLeverage`, настройка счёта):** предел суммарной позиции (`×` капитал) и сублинейный множитель одиночной сделки (`tierSizeMult`). В 1-м эшелоне предел одной бумаги растёт по закону `1 + 0,80 × (плечо − 1)`, оставляя часть бесплатного внутридневного плеча свободным резервом под параллельные идеи; во 2–3-м эшелоне прямой лимит (`17%` лонг / `7%` шорт на `1×`, наклон по плечу `0,72` / `0,40`) защищён потолком `15%` от 10-минутного оборота свечи (`barTurnoverRub`). Работает только при включённой маржинальной торговле; тариф «Инвестор» — без плеча, «Профи» — до 3, «VIP» и «Хозяин» — до 7. Предел заявки дополнительно проверяет брокер (`GetMaxLots`, маржинальный лимит).
|
|
159
161
|
6.2. **Стоп по общей просадке (`maxDrawdownPct`, настройка счёта, 0 — выкл.):** если оценка счёта опустилась от своего максимума на заданный процент, новые входы запрещаются до ручного снятия («Снять стоп по просадке»); открытые позиции сопровождаются. После вывода денег стоп снимается вручную.
|
|
160
162
|
6.3. **Капитал в LQDT как залог** (при включённых «Свободные рубли в LQDT» и маржинальной торговле): паи LQDT не продаются под входы — внутридневные позиции открываются под обеспечение и закрываются до ночи (платы за перенос нет), а свободные рубли размещаются в LQDT, когда у стратегии нет открытых позиций.
|
|
161
163
|
6.4. **Вход против планки:** если брокер отклонил вход из-за ценового коридора Мосбиржи (код 30099 — бумага у планки), вход в эту бумагу в ту же сторону блокируется до конца торгового дня.
|
|
@@ -171,13 +173,14 @@ $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
|
|
|
171
173
|
|
|
172
174
|
Изменение правил проверяется в лаборатории `scripts/strategy-lab.mjs` (варианты — в `scripts/strategy-lab-variants.mjs`) и принимается, только если оно лучше текущих правил **и на первой, и на второй половине периода**. Каждая отправленная заявка записывается в журнал решений `trade_decisions` (причина, ожидание и порог, контекст дня, плечо, версия программы; `GET /api/brokers/:id/decisions`).
|
|
173
175
|
|
|
174
|
-
Проверка правил 13.11.2026 (пошаговые прогнозы 06.2024–11.2026, 59 051 свеча,
|
|
176
|
+
Проверка правил 13.11.2026 (пошаговые прогнозы 06.2024–11.2026, 59 051 свеча, 896 сделок, капитал 800 тыс. ₽, LQDT за период — 18,5% годовых):
|
|
175
177
|
|
|
176
178
|
| Правила | CAGR (весь период) | 1-я / 2-я половина | PF | Макс. просадка | Худшая сделка, % капитала |
|
|
177
179
|
|---|---|---|---|---|---|
|
|
178
180
|
| До 06.11.2026 (без LQDT, 2-й эшелон полным размером) | 12,9% | 19,3% / 6,8% | 1,40 | 4,4% | −2,0% |
|
|
179
|
-
|
|
|
180
|
-
| Текущие (`
|
|
181
|
-
| Текущие (`
|
|
181
|
+
| Предыдущие (`v0.6.11`, полка `50%/38%` T1), плечо 2× | 59,8% | 73,8% / 47,4% | 2,03 | 3,6% | −1,9% |
|
|
182
|
+
| Текущие (живой `Conviction` + сублинейное плечо + перелив + потеснение), плечо 1× | **48,7%** | **59,9% / 38,7%** | **1,90** | **3,4%** | −1,3% |
|
|
183
|
+
| Текущие (живой `Conviction` + сублинейное плечо + перелив + потеснение), плечо 2× | **77,6%** | **95,0% / 62,4%** | **2,05** | **5,6%** | −2,4% |
|
|
184
|
+
| Текущие (живой `Conviction` + сублинейное плечо + перелив + потеснение), плечо 3× | **110,3%** | **133,7% / 90,1%** | **2,18** | **7,1%** | −3,4% |
|
|
182
185
|
|
|
183
186
|
Отклонены (хуже хотя бы на одной половине или с катастрофическим хвостом): защита вечным фьючерсом IMOEXF (издержки больше выигрыша), пассивные лимитные входы, снижение порога входа, 6–8 позиций на сторону, выход по времени, отключение фиксации прибыли, стоп по волатильности и «стоп отмены идеи», размер позиции по риску. Результат на истории не гарантирует будущий: плечо пропорционально увеличивает и доходность, и просадки.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/server/brokers/index.js
CHANGED
|
@@ -13,7 +13,10 @@ 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 {
|
|
16
|
+
import {
|
|
17
|
+
STRATEGY_POLICY, sessionEdgeWarning, evaluateProfitLock, protectiveStopPct, exitHoldRatio,
|
|
18
|
+
planSizingProfile, convictionMult, tierSizeMult, symbolCapRub, targetPositionRub, waterFillAllocations, MAX_LEVERAGE,
|
|
19
|
+
} from '../model/policy.js';
|
|
17
20
|
import { MODELS } from '../model/models.js';
|
|
18
21
|
import { blockingNews } from '../news/index.js';
|
|
19
22
|
import { round, MSK, DAY, fmtBarMsk } from '../util.js';
|
|
@@ -2093,32 +2096,25 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2093
2096
|
return isStockQuarantined(q.cnt, q.pnlPct / 100, edgeRatio);
|
|
2094
2097
|
};
|
|
2095
2098
|
const slots = dir === 'long' ? K : 2 * K;
|
|
2096
|
-
//
|
|
2097
|
-
// • Инвестор: 1 счёт до 1 млн ₽, только покупки (без шортов),
|
|
2098
|
-
// • Профи: до 5 счетов по 1 млн ₽, покупки + шорты,
|
|
2099
|
-
// • VIP: счета и сумма по договору,
|
|
2100
|
-
// • Хозяин: Conviction Sizing до
|
|
2099
|
+
// Профиль мощности слотов и индивидуальных потолков по тарифу (единый с бэктестом, server/model/policy.js):
|
|
2100
|
+
// • Инвестор: 1 счёт до 1 млн ₽, только покупки (без шортов), слот до 58% T1 / 14% T2
|
|
2101
|
+
// • Профи: до 5 счетов по 1 млн ₽, покупки + шорты, плечо до 3×, слот до 52% T1 / 12% T2
|
|
2102
|
+
// • VIP: счета и сумма по договору, Conviction Sizing до 75% T1 / 14% T2
|
|
2103
|
+
// • Хозяин: живой Conviction Sizing до 92% T1 лонг / 70% T1 шорт (с сублинейным ростом по плечу)
|
|
2101
2104
|
const isOwnerPlan = Boolean(plan.isOwner || plan.id === 'owner');
|
|
2105
|
+
const sizingProfile = planSizingProfile(isOwnerPlan ? 'owner' : (vipBoost ? 'vip' : (plan.id || 'investor')), dir);
|
|
2102
2106
|
let slotRub;
|
|
2103
|
-
if (
|
|
2104
|
-
|
|
2105
|
-
slotRub = budget * maxSlotWeight;
|
|
2106
|
-
} else if (vipBoost && autoAdapt) {
|
|
2107
|
-
const maxSlotWeight = Math.min(0.40, Math.max(1 / K, 1.4 / K));
|
|
2108
|
-
slotRub = budget * maxSlotWeight;
|
|
2107
|
+
if (autoAdapt) {
|
|
2108
|
+
slotRub = budget * sizingProfile.slotW;
|
|
2109
2109
|
} else if (plan.id === 'pro') {
|
|
2110
2110
|
slotRub = budget * Math.min(0.28, 1.15 / Math.max(2, slots));
|
|
2111
2111
|
} else {
|
|
2112
2112
|
slotRub = budget * Math.min(0.24, 1.05 / Math.max(3, K + 1));
|
|
2113
2113
|
}
|
|
2114
|
-
// (Потолки доходности по тарифам и «дроссель» размера позиций удалены: тарифы различаются возможностями,
|
|
2115
|
-
// а не обещанной доходностью — реалистичная доходность определяется стресс-тестом, а не тарифом.)
|
|
2116
2114
|
const needPct = Math.max(0.01, Number(fc.thresholdPct || 0));
|
|
2117
2115
|
const convOf = (r) => {
|
|
2118
2116
|
if (!autoAdapt || !r.expectedPct) return 1;
|
|
2119
|
-
|
|
2120
|
-
if (vipBoost) return Math.min(1.30, Math.max(0.85, 0.80 + 0.25 * (Math.abs(r.expectedPct) / needPct)));
|
|
2121
|
-
return 1;
|
|
2117
|
+
return convictionMult(Math.abs(r.expectedPct) / needPct, sizingProfile);
|
|
2122
2118
|
};
|
|
2123
2119
|
const heldMap = new Map((acc.lastSync?.positions || []).map((p) => [p.symbol, p]));
|
|
2124
2120
|
const rowMap = new Map((fc.rows || []).map((r) => [r.symbol, r]));
|
|
@@ -2315,67 +2311,21 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2315
2311
|
let usedRub = [...heldMap.values()].filter((p) => rowMap.has(p.symbol) && algoQtyOf(p.symbol, p.qty) !== 0 && !closing.has(p.symbol)).reduce((s, p) => s + Math.abs(algoQtyOf(p.symbol, p.qty) * p.price), 0)
|
|
2316
2312
|
+ (onlyEntries ? activePendingOrders.reduce((s, o) => s + Math.abs(Number(o.amountRub) || 0), 0) : 0);
|
|
2317
2313
|
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
if (!aQ) continue;
|
|
2332
|
-
const rCur = rowMap.get(sym);
|
|
2333
|
-
const curSignedPct = aQ > 0 ? (rCur.expectedPct || 0) : -(rCur.expectedPct || 0);
|
|
2334
|
-
if (curSignedPct < exitThresholdPct * 0.90 && curSignedPct < weakestEdge && bestNewPct > Math.max(needPct, curSignedPct) * 2.4) {
|
|
2335
|
-
weakestEdge = curSignedPct;
|
|
2336
|
-
weakestSym = sym;
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
if (weakestSym) {
|
|
2340
|
-
const wPos = heldMap.get(weakestSym);
|
|
2341
|
-
const wRow = rowMap.get(weakestSym);
|
|
2342
|
-
const wSp = specs[weakestSym] || { lotSize: 1, minStep: 0.01, decimals: 2 };
|
|
2343
|
-
const wAq = algoQtyOf(weakestSym, wPos.qty);
|
|
2344
|
-
const wLots = Math.floor(Math.abs(wAq) / wSp.lotSize);
|
|
2345
|
-
const wPx = Number(wRow?.price || wPos.price) || 0;
|
|
2346
|
-
if (wLots >= 1 && wPx > 0) {
|
|
2347
|
-
const isLongPos = wAq > 0;
|
|
2348
|
-
const limPx = sniperLimitPrice(wPx, isLongPos ? -1 : 1, acc.limitOffsetPct, wSp);
|
|
2349
|
-
orders.push({
|
|
2350
|
-
symbol: weakestSym,
|
|
2351
|
-
action: isLongPos ? 'sell' : 'cover',
|
|
2352
|
-
lotSize: wSp.lotSize,
|
|
2353
|
-
lots: wLots,
|
|
2354
|
-
shares: wLots * wSp.lotSize,
|
|
2355
|
-
refPrice: wPx,
|
|
2356
|
-
limitPrice: limPx,
|
|
2357
|
-
amountRub: round(wLots * wSp.lotSize * limPx, 2),
|
|
2358
|
-
reason: `Ротация капитала из остывшей бумаги (${wRow?.expectedPct > 0 ? '+' : ''}${wRow?.expectedPct}%) в нового сильного лидера (${bestNewPct}%)`,
|
|
2359
|
-
});
|
|
2360
|
-
closing.add(weakestSym);
|
|
2361
|
-
if (isLongPos) activeLongs = Math.max(0, activeLongs - 1);
|
|
2362
|
-
else activeShorts = Math.max(0, activeShorts - 1);
|
|
2363
|
-
usedRub = Math.max(0, usedRub - Math.abs(wAq * wPos.price));
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
const freedRub = orders.filter((o) => o.action === 'sell').reduce((s, o) => s + o.amountRub, 0);
|
|
2369
|
-
const coverSpentRub = orders.filter((o) => o.action === 'cover').reduce((s, o) => s + o.amountRub, 0);
|
|
2370
|
-
const openShortRub = [...heldMap.values()]
|
|
2371
|
-
.filter((p) => rowMap.has(p.symbol) && algoQtyOf(p.symbol, p.qty) < 0 && !closing.has(p.symbol))
|
|
2372
|
-
.reduce((s, p) => s + Math.abs(algoQtyOf(p.symbol, p.qty) * p.price), 0);
|
|
2373
|
-
const cashLimit = acc.lastSync?.cashRub !== undefined ? acc.lastSync.cashRub + freedRub - coverSpentRub - openShortRub : budget;
|
|
2374
|
-
let availRub = marginSizing
|
|
2375
|
-
? Math.max(0, budget * 0.99 * leverage - usedRub)
|
|
2376
|
-
: Math.min(Math.max(0, budget * 0.99 - usedRub), Math.max(0, cashLimit));
|
|
2314
|
+
const recomputeAvailRub = () => {
|
|
2315
|
+
const freed = orders.filter((o) => o.action === 'sell').reduce((s, o) => s + o.amountRub, 0);
|
|
2316
|
+
const coverSpent = orders.filter((o) => o.action === 'cover').reduce((s, o) => s + o.amountRub, 0);
|
|
2317
|
+
const trimmedShortRub = orders.filter((o) => o.action === 'cover' && o.isTrim).reduce((s, o) => s + o.amountRub, 0);
|
|
2318
|
+
const openShortRub = Math.max(0, [...heldMap.values()]
|
|
2319
|
+
.filter((p) => rowMap.has(p.symbol) && algoQtyOf(p.symbol, p.qty) < 0 && !closing.has(p.symbol))
|
|
2320
|
+
.reduce((s, p) => s + Math.abs(algoQtyOf(p.symbol, p.qty) * p.price), 0) - trimmedShortRub);
|
|
2321
|
+
const cashLim = acc.lastSync?.cashRub !== undefined ? acc.lastSync.cashRub + freed - coverSpent - openShortRub : budget;
|
|
2322
|
+
return marginSizing
|
|
2323
|
+
? Math.max(0, budget * 0.99 * leverage - usedRub)
|
|
2324
|
+
: Math.min(Math.max(0, budget * 0.99 - usedRub), Math.max(0, cashLim));
|
|
2325
|
+
};
|
|
2326
|
+
let availRub = recomputeAvailRub();
|
|
2377
2327
|
|
|
2378
|
-
|
|
2328
|
+
// 2) вход в лучшие сигналы (с учётом лотности Мосбиржи, свободного лимита и адаптивного отбора числа акций Dynamic Top-K)
|
|
2379
2329
|
const rawNewBuys = buyCands.filter((r) => !(algoQtyOf(r.symbol, heldMap.get(r.symbol)?.qty || 0) > 0 && !closing.has(r.symbol)) && !(onlyEntries && activeOrderSymbols.has(r.symbol)));
|
|
2380
2330
|
// Не открываем шорт по тикеру, в котором ещё лежит непроданный остаток старого портфеля (baseQty > 0), чтобы брокер не превратил шорт в продажу старой позиции
|
|
2381
2331
|
const rawNewShorts = shortCands.filter((r) => {
|
|
@@ -2395,30 +2345,53 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2395
2345
|
const newBuys = buyExhaustion.ready;
|
|
2396
2346
|
const newShorts = shortExhaustion.ready;
|
|
2397
2347
|
const deferredByImpulse = [...buyExhaustion.deferred, ...shortExhaustion.deferred];
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2348
|
+
const dynSlotRub = slotRub;
|
|
2349
|
+
|
|
2350
|
+
const netPickWeight = (r) => {
|
|
2351
|
+
const rawPct = Math.abs(r.expectedPct || 0);
|
|
2352
|
+
if (!autoAdapt) return Math.max(0.01, rawPct);
|
|
2353
|
+
const roundTripCostPct = TIER1_LIQUID_SYMBOLS.has(r.symbol) ? 0.13 : 0.25;
|
|
2354
|
+
return Math.max(0.01, rawPct - roundTripCostPct);
|
|
2355
|
+
};
|
|
2356
|
+
const calcDesiredNewRub = (r, side) => {
|
|
2357
|
+
const isTier1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
|
|
2358
|
+
const posSide = side === 'short' ? 'short' : 'long';
|
|
2359
|
+
const tierMult = autoAdapt ? tierSizeMult(r.symbol, leverage, posSide) : 1;
|
|
2360
|
+
const barTurnoverRub = !isTier1 ? recentBarTurnoverRub(r.symbol) : 0;
|
|
2361
|
+
const stopPct = protectiveStopPct(posSide);
|
|
2362
|
+
const effSlotRub = (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * tierMult;
|
|
2363
|
+
const targetRub = autoAdapt
|
|
2364
|
+
? targetPositionRub({ symbol: r.symbol, side: posSide, budget: budget * tierMult, slotRub: effSlotRub, conv: convOf(r), stopPct, barTurnoverRub, profile: sizingProfile })
|
|
2365
|
+
: Math.min(symbolCapRub(r.symbol, posSide, budget * tierMult, barTurnoverRub), effSlotRub);
|
|
2366
|
+
return { targetRub, tierMult, stopPct };
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
const combinedNew = [
|
|
2370
|
+
...newBuys.map((r) => ({ r, side: 'buy' })),
|
|
2371
|
+
...newShorts.map((r) => ({ r, side: 'short' })),
|
|
2372
|
+
];
|
|
2373
|
+
if (autoAdapt && combinedNew.length > 1) {
|
|
2374
|
+
combinedNew.sort((a, b) => netPickWeight(b.r) - netPickWeight(a.r));
|
|
2375
|
+
}
|
|
2376
|
+
const preparedNew = combinedNew.map(({ r, side }) => {
|
|
2377
|
+
const { targetRub, tierMult, stopPct } = calcDesiredNewRub(r, side);
|
|
2378
|
+
return { r, side, weight: netPickWeight(r), targetRub, tierMult, stopPct };
|
|
2379
|
+
}).filter((x) => x.targetRub > 5000);
|
|
2380
|
+
const totalDesiredNewRub = preparedNew.reduce((s, x) => s + x.targetRub, 0);
|
|
2405
2381
|
|
|
2406
2382
|
// В режиме «Плавный переход» (smooth):
|
|
2407
|
-
// • высвобождаем кэш из старого портфеля как под новые ПОКУПКИ, так и под новые
|
|
2408
|
-
// • даже если прямо сейчас нет нового входа, но свободного кэша на счёте меньше 1 рабочего слота, плавно продаём на каждом шаге по 1–2 самые слабые старые акции с отрицательным прогнозом (expectedPct <= -0.02%)
|
|
2409
|
-
// • сортируем кандидатов строго по худшему прогнозу ИИ (сначала акции с отрицательным ожиданием, среди близких — позиции в плюсе или с минимальным убытком), до 4 бумаг за шаг при активном сигнале.
|
|
2383
|
+
// • высвобождаем кэш из старого портфеля как под новые ПОКУПКИ, так и под новые ШОРТЫ;
|
|
2384
|
+
// • даже если прямо сейчас нет нового входа, но свободного кэша на счёте меньше 1 рабочего слота, плавно продаём на каждом шаге по 1–2 самые слабые старые акции с отрицательным прогнозом (expectedPct <= -0.02%).
|
|
2410
2385
|
if (legacyMode === 'smooth' && smoothSellPool.length > 0) {
|
|
2411
|
-
const activeEntries =
|
|
2412
|
-
const neededForSignalsRub = activeEntries.reduce((s, r) => s + (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * convOf(r), 0);
|
|
2386
|
+
const activeEntries = preparedNew.map((x) => x.r);
|
|
2413
2387
|
const hasWeakLegacy = smoothSellPool.some((x) => (x.r.expectedPct || 0) <= -0.02);
|
|
2414
2388
|
const proactiveReserveRub = (!activeEntries.length && hasWeakLegacy && availRub < slotRub) ? slotRub : 0;
|
|
2415
|
-
const targetNeedRub = Math.max(
|
|
2389
|
+
const targetNeedRub = Math.max(totalDesiredNewRub, proactiveReserveRub);
|
|
2416
2390
|
let shortfall = Math.min(Math.max(0, budget * 0.99 - usedRub), targetNeedRub) - availRub;
|
|
2417
2391
|
if (shortfall > 1000) {
|
|
2418
2392
|
smoothSellPool.sort((a, b) => {
|
|
2419
2393
|
const ea = Number(a.r.expectedPct || 0);
|
|
2420
2394
|
const eb = Number(b.r.expectedPct || 0);
|
|
2421
|
-
// Сначала бумаги с явно худшим прогнозом (разница > 0.03%), при близком прогнозе — сначала плюсовые/околонулевые
|
|
2422
2395
|
if (Math.abs(ea - eb) > 0.03) return ea - eb;
|
|
2423
2396
|
return ((b.pos.pnlRub >= 0 ? 1 : 0) - (a.pos.pnlRub >= 0 ? 1 : 0)) || (ea - eb);
|
|
2424
2397
|
});
|
|
@@ -2445,27 +2418,15 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2445
2418
|
}
|
|
2446
2419
|
}
|
|
2447
2420
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
...newShorts.map((r) => ({ r, side: 'short' })),
|
|
2452
|
-
];
|
|
2453
|
-
// Если включено «Свободные рубли в LQDT» (cashSweep) и для новых сигналов не хватает свободных рублей,
|
|
2454
|
-
// сначала автоматически продаём нужное число паёв фонда ликвидности LQDT, которые открыла стратегия (сверх неприкосновенного baselineQty)
|
|
2455
|
-
if (!collateral && acc.cashSweep && acc.broker === 'tbank' && combinedNew.length > 0 && !closing.has('LQDT') && !activeOrderSymbols.has('LQDT')) {
|
|
2421
|
+
|
|
2422
|
+
// Если включено «Свободные рубли в LQDT» (cashSweep) и счёт без залогового плеча (!collateral), при нехватке рублей продаём нужное число паёв LQDT
|
|
2423
|
+
if (!collateral && acc.cashSweep && acc.broker === 'tbank' && preparedNew.length > 0 && !closing.has('LQDT') && !activeOrderSymbols.has('LQDT')) {
|
|
2456
2424
|
const lqdtPos = heldMap.get('LQDT');
|
|
2457
2425
|
const lqdtAlgoQty = lqdtPos ? algoQtyOf('LQDT', lqdtPos.qty) : 0;
|
|
2458
2426
|
if (lqdtAlgoQty > 0 && lqdtPos.price > 0) {
|
|
2459
2427
|
const lqdtSp = specs.LQDT || { lotSize: 1, minStep: 0.001, decimals: 3 };
|
|
2460
2428
|
const lqdtLots = Math.floor(lqdtAlgoQty / lqdtSp.lotSize);
|
|
2461
|
-
const
|
|
2462
|
-
const isT1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
|
|
2463
|
-
const posSide = side === 'short' ? 'short' : 'long';
|
|
2464
|
-
const barTurnRub = !isT1 ? recentBarTurnoverRub(r.symbol) : 0;
|
|
2465
|
-
const liqCap = symbolCapRub(r.symbol, posSide, budget, barTurnRub);
|
|
2466
|
-
return s + Math.min(liqCap, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * convOf(r));
|
|
2467
|
-
}, 0);
|
|
2468
|
-
const needFromLqdtRub = Math.min(Math.max(0, budget * 0.99 - usedRub), neededForNewRub) - availRub;
|
|
2429
|
+
const needFromLqdtRub = Math.min(Math.max(0, budget * 0.99 - usedRub), totalDesiredNewRub) - availRub;
|
|
2469
2430
|
if (lqdtLots >= 1 && needFromLqdtRub > 1500) {
|
|
2470
2431
|
const limPx = sniperLimitPrice(lqdtPos.price, -1, 0.03, lqdtSp);
|
|
2471
2432
|
const lotVal = limPx * lqdtSp.lotSize;
|
|
@@ -2474,43 +2435,114 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2474
2435
|
orders.push({
|
|
2475
2436
|
symbol: 'LQDT', action: 'sell', lotSize: lqdtSp.lotSize, lots: sellLots, shares: sellLots * lqdtSp.lotSize,
|
|
2476
2437
|
refPrice: lqdtPos.price, limitPrice: limPx, amountRub,
|
|
2477
|
-
reason: `Высвобождение ${fmtRubSimple(amountRub)} ₽ из фонда денежного рынка LQDT под сигнал стратегии (${
|
|
2438
|
+
reason: `Высвобождение ${fmtRubSimple(amountRub)} ₽ из фонда денежного рынка LQDT под сигнал стратегии (${preparedNew.map((x) => x.r.symbol).join(', ')})`,
|
|
2478
2439
|
});
|
|
2479
2440
|
closing.add('LQDT');
|
|
2480
2441
|
availRub += amountRub;
|
|
2481
2442
|
}
|
|
2482
2443
|
}
|
|
2483
2444
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2445
|
+
|
|
2446
|
+
// Двухступенчатое динамическое потеснение под новый сильный сигнал 1-го эшелона (единое с server/model/simulator.js):
|
|
2447
|
+
// в режиме auto работает и на быстрых внутрисвечных входах, а в режиме confirm — на плановых 30-минутных шагах (!onlyEntries),
|
|
2448
|
+
// ротируя остывшие позиции либо частично подрезая позицию 2-го эшелона (с сохранением entryTs и peakPrice оставшихся лотов).
|
|
2449
|
+
if (autoAdapt && STRATEGY_POLICY.crowdOut !== false && (acc.mode === 'auto' || !onlyEntries) && preparedNew.length > 0) {
|
|
2450
|
+
const bestT1 = preparedNew.find((x) => TIER1_LIQUID_SYMBOLS.has(x.r.symbol));
|
|
2451
|
+
const minChunkRub = Math.max(45000, budget * (STRATEGY_POLICY.crowdMinPct ?? 0.06));
|
|
2452
|
+
let shortfall = totalDesiredNewRub - availRub;
|
|
2453
|
+
if (bestT1 && shortfall >= minChunkRub) {
|
|
2454
|
+
const minHoldMs = (STRATEGY_POLICY.crowdMinMin ?? 20) * 60000;
|
|
2455
|
+
const rotBufPct = STRATEGY_POLICY.crowdRotBufPct ?? 0.08;
|
|
2456
|
+
const trimBufPct = STRATEGY_POLICY.crowdTrimBufPct ?? 0.12;
|
|
2457
|
+
const elig = [];
|
|
2458
|
+
for (const [sym, pos] of heldMap) {
|
|
2459
|
+
if (closing.has(sym) || activeOrderSymbols.has(sym) || !rowMap.has(sym)) continue;
|
|
2460
|
+
const aQ = algoQtyOf(sym, pos.qty);
|
|
2461
|
+
if (!aQ) continue;
|
|
2462
|
+
// На безналичном счёте без маржиналки закрытие шорта тратит кэш, а не высвобождает его под покупку
|
|
2463
|
+
if (!marginSizing && aQ < 0) continue;
|
|
2464
|
+
const entryAt = Number(acc.entryTsBySymbol?.[sym]) || 0;
|
|
2465
|
+
if (entryAt > 0 && nowTs - entryAt < minHoldMs) continue;
|
|
2466
|
+
const sp = specs[sym] || { lotSize: 1, minStep: 0.01, decimals: 2 };
|
|
2467
|
+
const totalLots = Math.floor(Math.abs(aQ) / sp.lotSize);
|
|
2468
|
+
const rCur = rowMap.get(sym);
|
|
2469
|
+
const px = Number(rCur?.price || pos.price) || 0;
|
|
2470
|
+
if (totalLots < 1 || !(px > 0)) continue;
|
|
2471
|
+
const isLongPos = aQ > 0;
|
|
2472
|
+
const curSignedPct = isLongPos ? Number(rCur?.expectedPct || 0) : -Number(rCur?.expectedPct || 0);
|
|
2473
|
+
const thrEntryPct = isLongPos ? reqBuyEdgeOf(sym) : reqShortEdgeOf(sym);
|
|
2474
|
+
const heldIsT1 = TIER1_LIQUID_SYMBOLS.has(sym);
|
|
2475
|
+
elig.push({ sym, pos, rCur, sp, totalLots, px, isLongPos, curSignedPct, thrEntryPct, heldIsT1, posRub: totalLots * sp.lotSize * px, tierRank: heldIsT1 ? 1 : 0 });
|
|
2476
|
+
}
|
|
2477
|
+
elig.sort((a, b) => (a.tierRank - b.tierRank) || (a.curSignedPct - b.curSignedPct));
|
|
2478
|
+
|
|
2479
|
+
for (const el of elig) {
|
|
2480
|
+
if (shortfall < minChunkRub * 0.5) break;
|
|
2481
|
+
const coolRatio = el.heldIsT1 ? (STRATEGY_POLICY.crowdCoolRatioT1 ?? 0.45) : (STRATEGY_POLICY.crowdCoolRatioT2 ?? 0.80);
|
|
2482
|
+
const cooled = el.curSignedPct < el.thrEntryPct * coolRatio;
|
|
2483
|
+
const netAdvPct = bestT1.weight - Math.max(0, el.curSignedPct);
|
|
2484
|
+
const limPx = sniperLimitPrice(el.px, el.isLongPos ? -1 : 1, acc.limitOffsetPct, el.sp);
|
|
2485
|
+
const lotVal = limPx * el.sp.lotSize;
|
|
2486
|
+
if (!(lotVal > 0)) continue;
|
|
2487
|
+
|
|
2488
|
+
let lotsToClose = 0;
|
|
2489
|
+
let fullRotate = false;
|
|
2490
|
+
if (cooled && netAdvPct >= rotBufPct) {
|
|
2491
|
+
if (!el.heldIsT1 || el.curSignedPct < el.thrEntryPct * 0.35 || el.posRub - shortfall < minChunkRub) {
|
|
2492
|
+
lotsToClose = el.totalLots;
|
|
2493
|
+
fullRotate = true;
|
|
2494
|
+
} else {
|
|
2495
|
+
const maxTrimRub = el.posRub - minChunkRub;
|
|
2496
|
+
const trimRub = Math.min(shortfall, maxTrimRub);
|
|
2497
|
+
if (trimRub >= minChunkRub) lotsToClose = Math.min(el.totalLots - 1, Math.max(1, Math.ceil(trimRub / lotVal)));
|
|
2498
|
+
}
|
|
2499
|
+
} else if (!cooled && !el.heldIsT1 && netAdvPct >= trimBufPct && el.posRub >= minChunkRub * 1.5) {
|
|
2500
|
+
const maxTrimRub = el.posRub - minChunkRub;
|
|
2501
|
+
const trimRub = Math.min(shortfall, maxTrimRub);
|
|
2502
|
+
if (trimRub >= minChunkRub) lotsToClose = Math.min(el.totalLots - 1, Math.max(1, Math.ceil(trimRub / lotVal)));
|
|
2503
|
+
}
|
|
2504
|
+
if (lotsToClose >= 1) {
|
|
2505
|
+
const isTrim = !fullRotate && lotsToClose < el.totalLots;
|
|
2506
|
+
const amountRub = round(lotsToClose * el.sp.lotSize * limPx, 2);
|
|
2507
|
+
const freedExpRub = lotsToClose * el.sp.lotSize * el.pos.price;
|
|
2508
|
+
orders.push({
|
|
2509
|
+
symbol: el.sym,
|
|
2510
|
+
action: el.isLongPos ? 'sell' : 'cover',
|
|
2511
|
+
lotSize: el.sp.lotSize,
|
|
2512
|
+
lots: lotsToClose,
|
|
2513
|
+
shares: lotsToClose * el.sp.lotSize,
|
|
2514
|
+
refPrice: el.px,
|
|
2515
|
+
limitPrice: limPx,
|
|
2516
|
+
amountRub,
|
|
2517
|
+
isTrim,
|
|
2518
|
+
urgentExit: true,
|
|
2519
|
+
reason: isTrim
|
|
2520
|
+
? `Частичное потеснение (${lotsToClose} из ${el.totalLots} лот., прогноз ${el.rCur?.expectedPct > 0 ? '+' : ''}${el.rCur?.expectedPct}%) под сильный сигнал 1-го эшелона ${bestT1.r.symbol} (${bestT1.r.expectedPct > 0 ? '+' : ''}${bestT1.r.expectedPct}%)`
|
|
2521
|
+
: `Ротация капитала из остывшей бумаги (${el.rCur?.expectedPct > 0 ? '+' : ''}${el.rCur?.expectedPct}%) в флагман 1-го эшелона ${bestT1.r.symbol} (${bestT1.r.expectedPct > 0 ? '+' : ''}${bestT1.r.expectedPct}%)`,
|
|
2522
|
+
});
|
|
2523
|
+
if (!isTrim) {
|
|
2524
|
+
closing.add(el.sym);
|
|
2525
|
+
if (el.isLongPos) activeLongs = Math.max(0, activeLongs - 1);
|
|
2526
|
+
else activeShorts = Math.max(0, activeShorts - 1);
|
|
2527
|
+
}
|
|
2528
|
+
usedRub = Math.max(0, usedRub - freedExpRub);
|
|
2529
|
+
availRub = recomputeAvailRub();
|
|
2530
|
+
shortfall = totalDesiredNewRub - availRub;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2492
2534
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
//
|
|
2496
|
-
//
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
const
|
|
2502
|
-
remWeightSum = Math.max(0, remWeightSum - wCur);
|
|
2503
|
-
remNew--;
|
|
2535
|
+
|
|
2536
|
+
// Итеративное распределение свободного ресурса методом перелива (waterFillAllocations в policy.js):
|
|
2537
|
+
// кандидаты с узким потолком (2-й эшелон / лимит 15% от 10-минутного оборота свечи) забирают ровно свой потолок,
|
|
2538
|
+
// а весь оставшийся лимит без холостого простоя переливается флагманским лидерам 1-го эшелона.
|
|
2539
|
+
const allocSums = autoAdapt
|
|
2540
|
+
? waterFillAllocations(preparedNew, availRub)
|
|
2541
|
+
: Float64Array.from(preparedNew.map((x) => Math.min(x.targetRub, availRub / Math.max(1, preparedNew.length))));
|
|
2542
|
+
for (let idx = 0; idx < preparedNew.length; idx++) {
|
|
2543
|
+
const { r, side, targetRub, tierMult, stopPct } = preparedNew[idx];
|
|
2504
2544
|
const isTier1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
|
|
2505
|
-
const
|
|
2506
|
-
// множитель размера по эшелону и направлению: 1-й эшелон — плечо счёта, 2–3-й — асимметричное масштабирование от плеча (policy.js)
|
|
2507
|
-
const tierMult = autoAdapt ? tierSizeMult(r.symbol, leverage, posSide) : 1;
|
|
2508
|
-
// Лимит от реального 10-минутного оборота стакана (Liquidity Cap): во 2-м эшелоне не входим объёмом больше 15% от 10-минутного оборота биржи,
|
|
2509
|
-
// чтобы заявка не становилась «стенкой» для стаканных роботов и исполнялась целиком без проскальзывания
|
|
2510
|
-
const barTurnoverRub = !isTier1 ? recentBarTurnoverRub(r.symbol) : 0;
|
|
2511
|
-
const sideCapRub = symbolCapRub(r.symbol, posSide, budget * tierMult, barTurnoverRub);
|
|
2512
|
-
const rawSum = Math.min(sideCapRub, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * tierMult * convOf(r));
|
|
2513
|
-
const sum = Math.min(rawSum, propShare, availRub);
|
|
2545
|
+
const sum = Math.min(allocSums[idx], availRub);
|
|
2514
2546
|
const refPx = r.price || specs[r.symbol]?.lastPrice || 0;
|
|
2515
2547
|
const effOffsetPct = isTier1 ? Math.min(Number(acc.limitOffsetPct) || 0.05, 0.06) : Math.min(Number(acc.limitOffsetPct) || 0.04, 0.04);
|
|
2516
2548
|
const ord = calcLotOrder(r.symbol, sum, refPx, side, effOffsetPct, specs);
|
|
@@ -2524,8 +2556,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2524
2556
|
bar: fc.bar, expectedPct: r.expectedPct,
|
|
2525
2557
|
thresholdPct: side === 'buy' ? reqBuyEdgeOf(r.symbol) : reqShortEdgeOf(r.symbol),
|
|
2526
2558
|
edgeMult: r.edgeMult, sigmaBarPct: r.sigmaPct, lastMovePct: r.lastMovePct, selfPct: r.selfPct,
|
|
2527
|
-
marketDown: Boolean(fc.marketDown), conv: round(convOf(r), 3), budgetRub: budget, targetRub: round(
|
|
2528
|
-
stopPct
|
|
2559
|
+
marketDown: Boolean(fc.marketDown), conv: round(convOf(r), 3), budgetRub: budget, targetRub: round(targetRub, 0), leverage, tierMult, collateral,
|
|
2560
|
+
stopPct, day: r.day || null, onlyEntries: Boolean(onlyEntries),
|
|
2529
2561
|
},
|
|
2530
2562
|
reason: side === 'buy'
|
|
2531
2563
|
? `Покупка (${MODELS[params.modelType]?.label || params.modelType}): ожидание ${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}% за ${params.modelHorizon} ч`
|
|
@@ -2783,7 +2815,7 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
2783
2815
|
// при узком спреде (<= 0,10% или <= 2 шагов) ставим ровно в лучшую встречную цену (bestAsk / bestBid),
|
|
2784
2816
|
// а при широком спреде встаём на середине спреда (midPx), не переплачивая стаканным роботам!
|
|
2785
2817
|
const tightExitSpread = spreadPct <= 0.10 || (bestAsk - bestBid) <= 2.1 * step;
|
|
2786
|
-
if (ord.isStopLoss && spreadPct <= 0.80) {
|
|
2818
|
+
if ((ord.isStopLoss || ord.urgentExit) && spreadPct <= (ord.isStopLoss ? 0.80 : 0.35)) {
|
|
2787
2819
|
const mktExitPx = roundToStep(isBuy ? bestAsk : bestBid, step, sp.decimals);
|
|
2788
2820
|
if (mktExitPx > 0) finalLimitPx = isBuy ? Math.max(finalLimitPx, mktExitPx) : Math.min(finalLimitPx, mktExitPx);
|
|
2789
2821
|
} else if (tightExitSpread) {
|
|
@@ -3175,10 +3207,12 @@ export async function executeBrokerOrders(id, { _lockHeld = false } = {}) {
|
|
|
3175
3207
|
delete acc.entryFailReasonBySymbol[ord.symbol];
|
|
3176
3208
|
} else if (ord.action === 'sell' || ord.action === 'cover') {
|
|
3177
3209
|
hadExits = true;
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3210
|
+
if (!ord.isTrim) {
|
|
3211
|
+
delete acc.entryTsBySymbol[ord.symbol];
|
|
3212
|
+
delete acc.peakPriceBySymbol[ord.symbol];
|
|
3213
|
+
if (ord.symbol !== 'LQDT') {
|
|
3214
|
+
acc.exitCooldownBySymbol[ord.symbol] = ord.isStopLoss ? execNowTs : (execNowTs - 30 * 60000);
|
|
3215
|
+
}
|
|
3182
3216
|
}
|
|
3183
3217
|
}
|
|
3184
3218
|
} catch (e) {
|
|
@@ -3513,8 +3547,9 @@ async function tickBrokerSchedule() {
|
|
|
3513
3547
|
const lqdtBaseQty = Number(acc.baselineQty?.LQDT || 0);
|
|
3514
3548
|
const lqdtAvailRub = lqdtPos ? Math.max(0, (lqdtPos.qty - (acc.legacyMode === 'takeover' ? 0 : Math.max(0, lqdtBaseQty))) * (lqdtPos.price || 0)) : 0;
|
|
3515
3549
|
const freeCashRub = Number(acc.lastSync?.cashRub ?? acc.maxCapitalRub ?? 0) + lqdtAvailRub;
|
|
3550
|
+
const canEnterOrCrowd = freeCashRub >= 15000 || (acc.marginEnabled && (Number(acc.maxLeverage) || 1) > 1) || hasOpenAlgoPos;
|
|
3516
3551
|
const marketActiveNow = Boolean(latest && nowTs - latest <= 25 * 60000);
|
|
3517
|
-
if (marketActiveNow && acc.mode === 'auto' && acc.token && !acc.haltedReason && !(acc.skipEdgeMinutes && sessionEdgeWarning(nowTs)) &&
|
|
3552
|
+
if (marketActiveNow && acc.mode === 'auto' && acc.token && !acc.haltedReason && !(acc.skipEdgeMinutes && sessionEdgeWarning(nowTs)) && canEnterOrCrowd && nowTs - Math.max(lastFullWallTs, lastAnyWallTs) >= 20000) {
|
|
3518
3553
|
await prepareBrokerOrders(acc.id, { onlyEntries: true }).catch(() => {});
|
|
3519
3554
|
}
|
|
3520
3555
|
}
|
package/server/model/policy.js
CHANGED
|
@@ -19,28 +19,44 @@ export const STRATEGY_POLICY = Object.freeze({
|
|
|
19
19
|
profitLock: true,
|
|
20
20
|
// размер позиции: 'slot' — доля бюджета × сила сигнала
|
|
21
21
|
sizing: Object.freeze({ mode: 'slot' }),
|
|
22
|
-
//
|
|
23
|
-
// • 1-й эшелон (33 ликвидные акции,
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// •
|
|
30
|
-
// •
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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,
|
|
40
48
|
turnoverCapMult: 0.15,
|
|
41
49
|
tpRetainT2: 0.75,
|
|
42
50
|
tpTrailT2: 0.35,
|
|
43
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,
|
|
44
60
|
// капитал хранится в фонде денежного рынка LQDT как залог: внутридневные позиции открываются под обеспечение
|
|
45
61
|
// (без платы за перенос — позиции закрываются до ночи), LQDT не продаётся под входы
|
|
46
62
|
cashYield: true,
|
|
@@ -141,44 +157,166 @@ export function exitHoldRatio(heldMin, horizonMin, { sigmaBarPct = 0, realizedBa
|
|
|
141
157
|
return startRatio + (endRatio - startRatio) * ramp;
|
|
142
158
|
}
|
|
143
159
|
|
|
144
|
-
// ---------- плечо и
|
|
160
|
+
// ---------- профиль слотов по тарифу, плечо и предельный размер позиции ----------
|
|
145
161
|
export const MAX_LEVERAGE = 7;
|
|
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
|
+
// разгружает риск одиночной бумаги на высоких плечах и оставляет свободный маржинальный резерв под параллельные идеи.
|
|
146
222
|
export function tierSizeMult(symbol, leverage = 1, sideOrPolicy = 'long', policyArg = STRATEGY_POLICY) {
|
|
147
223
|
const isObj = sideOrPolicy && typeof sideOrPolicy === 'object';
|
|
148
224
|
const side = isObj ? 'long' : (sideOrPolicy === 'short' ? 'short' : 'long');
|
|
149
225
|
const policy = isObj ? sideOrPolicy : (policyArg || STRATEGY_POLICY);
|
|
150
226
|
const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
242
|
}
|
|
158
243
|
|
|
159
244
|
// ---------- предельный размер позиции на одну бумагу ----------
|
|
160
|
-
export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY) {
|
|
245
|
+
export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY, profile = null) {
|
|
161
246
|
const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
|
|
162
247
|
const isShort = side === 'short';
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
263
|
const rawCap = budget * capPct;
|
|
167
264
|
const turnMult = policy?.turnoverCapMult ?? 0.15;
|
|
168
265
|
return (!isT1 && barTurnoverRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnoverRub * turnMult)) : rawCap;
|
|
169
266
|
}
|
|
170
267
|
|
|
171
|
-
// Размер новой позиции в рублях до учёта свободного
|
|
172
|
-
// 'slot' — рабочий слот × сила сигнала (conv), не больше предела бумаги;
|
|
268
|
+
// Размер новой позиции в рублях до учёта свободного лимита и итеративного перелива (water-filling):
|
|
269
|
+
// 'slot' — рабочий слот × множитель эшелона × сила сигнала (conv), не больше предела бумаги;
|
|
173
270
|
// 'risk' — при срабатывании стопа счёт теряет не больше riskPct% бюджета.
|
|
174
|
-
export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0 }, policy = STRATEGY_POLICY) {
|
|
175
|
-
const cap = symbolCapRub(symbol, side, budget, barTurnoverRub, policy);
|
|
176
|
-
const sz = policy
|
|
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 || {};
|
|
177
274
|
if (sz.mode === 'risk' && stopPct > 0) {
|
|
178
275
|
const byRisk = (budget * (sz.riskPct / 100)) / (stopPct / 100);
|
|
179
276
|
return Math.min(cap, byRisk * (sz.useConv ? conv : 1), sz.maxSlotPct ? budget * sz.maxSlotPct : Infinity);
|
|
180
277
|
}
|
|
181
|
-
|
|
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;
|
|
182
320
|
}
|
|
183
321
|
|
|
184
322
|
// ---------- контекст торгового дня бумаги по свечам до момента решения ----------
|
package/server/model/research.js
CHANGED
|
@@ -41,7 +41,8 @@ export { isMarketDown };
|
|
|
41
41
|
// 17: капитал в LQDT как залог, половинный размер 2-го эшелона, строки с плечом 3 и 5
|
|
42
42
|
// 18: глубина сопровождения позиции и вечернего окна входов holdBars=13 (130 мин) во всех контурах симуляции
|
|
43
43
|
// 19: ревизия правил 1-го и 2-го эшелонов (MTLR/BSPB/RTKM/SVCB в T1, асимметричный масштаб T2 от плеча, лимит 50%/38% T1 и плотный трал T2)
|
|
44
|
-
|
|
44
|
+
// 20: разблокировка Conviction Sizing в T1 (до 92%/70% с сублинейным плечом 0,80), прямой риск-лимит T2, перелив water-filling и двухступенчатое потеснение
|
|
45
|
+
const DATA_VERSION = 20;
|
|
45
46
|
|
|
46
47
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
47
48
|
function reuseParamsKey(params) {
|
|
@@ -822,7 +823,7 @@ async function run(runId, p, state) {
|
|
|
822
823
|
const lqdtRows = await ensureLqdtHistory().catch(() => []);
|
|
823
824
|
const lqdt = lqdtRows.length ? alignSeries(timeline, lqdtRows) : null;
|
|
824
825
|
const liveRows = [];
|
|
825
|
-
for (const lev of [1, 3, 5]) {
|
|
826
|
+
for (const lev of [1, 2, 3, 5]) {
|
|
826
827
|
const lp = simulateStrategy({ ds, wf: wfLive, p, capital: p.capital, shortable: shortableSet(), divCal, lqdt, leverage: lev });
|
|
827
828
|
const yrs = Math.max(1 / 365, ((lp.curve.at(-1)?.ts || 0) - (lp.curve[0]?.ts || 0)) / (365.25 * 86400000));
|
|
828
829
|
const lastLpTs = lp.curve.at(-1)?.ts || 0;
|
|
@@ -837,7 +838,7 @@ async function run(runId, p, state) {
|
|
|
837
838
|
liveRows.push({
|
|
838
839
|
key: lev === 1 ? 'liveRules' : `liveRulesL${lev}`,
|
|
839
840
|
label: lev === 1 ? 'Правила живого контура, без плеча (главная оценка)' : `Правила живого контура, плечо ${lev}`,
|
|
840
|
-
note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы,
|
|
841
|
+
note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы, итеративный перелив (water-filling), двухступенчатое потеснение и издержки по эшелонам${lqdt ? ', капитал в LQDT как залог' : ''}${lev > 1 ? `; сублинейный масштаб одиночной позиции при лимите портфеля ×${lev}` : ''}`,
|
|
841
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) },
|
|
842
843
|
costs: null,
|
|
843
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 {
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
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]);
|
|
@@ -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
|
-
|
|
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,28 +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
|
-
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
444
|
picks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
|
|
330
|
-
|
|
331
|
-
|
|
445
|
+
avail = Math.max(0, budget * maxGross - grossUsed(t));
|
|
446
|
+
const prepared = [];
|
|
332
447
|
for (const [i, side] of picks) {
|
|
333
448
|
const e = edge(i);
|
|
334
|
-
const w = pickWeight(i);
|
|
335
|
-
const propShare = remW > 0 ? avail * (w / remW) : avail;
|
|
336
|
-
remW -= w;
|
|
337
449
|
const sigmaBarPct = sg[i] * 100;
|
|
338
450
|
const ctx = dayContext(ds, i, t, sigmaBarPct);
|
|
339
451
|
if (counterTrendBlocked(side, ctx, policy)) { stats.blockedCT++; continue; }
|
|
340
452
|
const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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);
|
|
346
475
|
if (!(alloc > 5000)) continue;
|
|
347
476
|
const tk = tokens[i];
|
|
348
477
|
let entryPx = fill === 'mid' ? (tk.open[t + 1] + tk.close[t + 1]) / 2 : tk.open[t + 1];
|
|
@@ -260,12 +260,13 @@ ${liveTradingSummary}
|
|
|
260
260
|
- Базовый порог выгоды на вход = 2,5 × 2 × (комиссия + проскальзывание) × 1,60. Для 1-го эшелона при настройке издержек 0,05% базовый порог составляет **+0,40%** (с тонкой поправкой symbolEdgeMult на оборот и волатильность бумаги в коридоре 0,88..1,45).
|
|
261
261
|
- Для 2–3-го эшелона порог входа повышен в **1,20× (TIER2_EDGE_MULT)** — до **~+0,48%**, чтобы ожидаемый импульс с запасом перекрывал более широкий спред стакана. В первые 20 минут утренней сессии (07:00–07:20 МСК) шорты 2-го эшелона блокируются из-за пустого утреннего стакана.
|
|
262
262
|
- Авто-балансировка по тренду индекса (isMarketDown): на растущем рынке покупки открываются от базового порога (+0,40% 1-й эшелон и +0,48% 2-й эшелон), а контртрендовые шорты ужесточаются в 1,25× (до −0,50% и −0,60%). На падающем рынке — наоборот: покупки ужесточаются в 1,25×, а шорты облегчаются в 0,90×. Всегда называй пользователю точный индивидуальный порог конкретной бумаги (\`порог_покупки_проц\` / \`порог_шорта_проц\`).
|
|
263
|
-
- **Размер позиций,
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
1. В **1-м эшелоне**
|
|
267
|
-
2. Во **2-м эшелоне** действует
|
|
268
|
-
-
|
|
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-минутных шагах).
|
|
269
270
|
- **Капитал в фонде денежного рынка LQDT как залог:** свободный капитал работает в LQDT, а внутридневные позиции открываются под обеспечение и закрываются до ночи (без платы брокеру за перенос через ночь).
|
|
270
271
|
- **Глубина сопровождения позиций (130 минут), фиксация прибыли и защитные контуры:**
|
|
271
272
|
- **Глубина сопровождения (holdBars = 13 свечей = 130 минут = 2 ч 10 м):** плановая ревизия портфеля идёт каждые 20–30 минут (holdBars / 5), а вечернее окно новых внутридневных входов закрывается за 130 минут до конца торгов (после **21:30 МСК** новые входы не открываются, перед 23:40–23:50 МСК позиции закрываются перед ночью).
|