neuroalpha 0.6.11 → 0.6.13
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 -146
- package/server/index.js +33 -8
- package/server/model/dataset.js +2 -4
- package/server/model/forecast.js +1 -3
- package/server/model/policy.js +173 -35
- package/server/model/research.js +4 -3
- package/server/model/simulator.js +148 -19
- package/server/news/index.js +2 -3
- 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.13",
|
|
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';
|
|
@@ -188,7 +191,6 @@ function logBrokerEvent(acc, kind, text) {
|
|
|
188
191
|
const uid = currentUserId();
|
|
189
192
|
db.prepare('INSERT INTO broker_events (ts, user_id, account_id, kind, text) VALUES (?, ?, ?, ?, ?)')
|
|
190
193
|
.run(Date.now(), uid, String(acc?.id || ''), String(kind), String(text || '').slice(0, 500));
|
|
191
|
-
if (/error|stop|halt/.test(kind)) console.warn(`[брокер ${acc?.name || acc?.id}] ${kind}: ${text}`);
|
|
192
194
|
// храним последние 5000 событий на счёт
|
|
193
195
|
if (Math.random() < 0.02) {
|
|
194
196
|
db.prepare(`DELETE FROM broker_events WHERE user_id = ? AND account_id = ? AND id NOT IN
|
|
@@ -272,7 +274,6 @@ function migrateKvAccounts(uid) {
|
|
|
272
274
|
db.exec('COMMIT');
|
|
273
275
|
} catch (e) { db.exec('ROLLBACK'); throw e; }
|
|
274
276
|
// прежняя запись не удаляется: при откате на версию до 0.4.1 счета останутся на месте (в состоянии на момент переноса)
|
|
275
|
-
console.log(`✓ Брокерские счета пользователя ${uid} перенесены в отдельные записи (${old.length})`);
|
|
276
277
|
}
|
|
277
278
|
userKvSet('brokerAccountsMigrated', true, uid);
|
|
278
279
|
}
|
|
@@ -2093,32 +2094,25 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2093
2094
|
return isStockQuarantined(q.cnt, q.pnlPct / 100, edgeRatio);
|
|
2094
2095
|
};
|
|
2095
2096
|
const slots = dir === 'long' ? K : 2 * K;
|
|
2096
|
-
//
|
|
2097
|
-
// • Инвестор: 1 счёт до 1 млн ₽, только покупки (без шортов),
|
|
2098
|
-
// • Профи: до 5 счетов по 1 млн ₽, покупки + шорты,
|
|
2099
|
-
// • VIP: счета и сумма по договору,
|
|
2100
|
-
// • Хозяин: Conviction Sizing до
|
|
2097
|
+
// Профиль мощности слотов и индивидуальных потолков по тарифу (единый с бэктестом, server/model/policy.js):
|
|
2098
|
+
// • Инвестор: 1 счёт до 1 млн ₽, только покупки (без шортов), слот до 58% T1 / 14% T2
|
|
2099
|
+
// • Профи: до 5 счетов по 1 млн ₽, покупки + шорты, плечо до 3×, слот до 52% T1 / 12% T2
|
|
2100
|
+
// • VIP: счета и сумма по договору, Conviction Sizing до 75% T1 / 14% T2
|
|
2101
|
+
// • Хозяин: живой Conviction Sizing до 92% T1 лонг / 70% T1 шорт (с сублинейным ростом по плечу)
|
|
2101
2102
|
const isOwnerPlan = Boolean(plan.isOwner || plan.id === 'owner');
|
|
2103
|
+
const sizingProfile = planSizingProfile(isOwnerPlan ? 'owner' : (vipBoost ? 'vip' : (plan.id || 'investor')), dir);
|
|
2102
2104
|
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;
|
|
2105
|
+
if (autoAdapt) {
|
|
2106
|
+
slotRub = budget * sizingProfile.slotW;
|
|
2109
2107
|
} else if (plan.id === 'pro') {
|
|
2110
2108
|
slotRub = budget * Math.min(0.28, 1.15 / Math.max(2, slots));
|
|
2111
2109
|
} else {
|
|
2112
2110
|
slotRub = budget * Math.min(0.24, 1.05 / Math.max(3, K + 1));
|
|
2113
2111
|
}
|
|
2114
|
-
// (Потолки доходности по тарифам и «дроссель» размера позиций удалены: тарифы различаются возможностями,
|
|
2115
|
-
// а не обещанной доходностью — реалистичная доходность определяется стресс-тестом, а не тарифом.)
|
|
2116
2112
|
const needPct = Math.max(0.01, Number(fc.thresholdPct || 0));
|
|
2117
2113
|
const convOf = (r) => {
|
|
2118
2114
|
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;
|
|
2115
|
+
return convictionMult(Math.abs(r.expectedPct) / needPct, sizingProfile);
|
|
2122
2116
|
};
|
|
2123
2117
|
const heldMap = new Map((acc.lastSync?.positions || []).map((p) => [p.symbol, p]));
|
|
2124
2118
|
const rowMap = new Map((fc.rows || []).map((r) => [r.symbol, r]));
|
|
@@ -2315,67 +2309,21 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2315
2309
|
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
2310
|
+ (onlyEntries ? activePendingOrders.reduce((s, o) => s + Math.abs(Number(o.amountRub) || 0), 0) : 0);
|
|
2317
2311
|
|
|
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));
|
|
2312
|
+
const recomputeAvailRub = () => {
|
|
2313
|
+
const freed = orders.filter((o) => o.action === 'sell').reduce((s, o) => s + o.amountRub, 0);
|
|
2314
|
+
const coverSpent = orders.filter((o) => o.action === 'cover').reduce((s, o) => s + o.amountRub, 0);
|
|
2315
|
+
const trimmedShortRub = orders.filter((o) => o.action === 'cover' && o.isTrim).reduce((s, o) => s + o.amountRub, 0);
|
|
2316
|
+
const openShortRub = Math.max(0, [...heldMap.values()]
|
|
2317
|
+
.filter((p) => rowMap.has(p.symbol) && algoQtyOf(p.symbol, p.qty) < 0 && !closing.has(p.symbol))
|
|
2318
|
+
.reduce((s, p) => s + Math.abs(algoQtyOf(p.symbol, p.qty) * p.price), 0) - trimmedShortRub);
|
|
2319
|
+
const cashLim = acc.lastSync?.cashRub !== undefined ? acc.lastSync.cashRub + freed - coverSpent - openShortRub : budget;
|
|
2320
|
+
return marginSizing
|
|
2321
|
+
? Math.max(0, budget * 0.99 * leverage - usedRub)
|
|
2322
|
+
: Math.min(Math.max(0, budget * 0.99 - usedRub), Math.max(0, cashLim));
|
|
2323
|
+
};
|
|
2324
|
+
let availRub = recomputeAvailRub();
|
|
2377
2325
|
|
|
2378
|
-
|
|
2326
|
+
// 2) вход в лучшие сигналы (с учётом лотности Мосбиржи, свободного лимита и адаптивного отбора числа акций Dynamic Top-K)
|
|
2379
2327
|
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
2328
|
// Не открываем шорт по тикеру, в котором ещё лежит непроданный остаток старого портфеля (baseQty > 0), чтобы брокер не превратил шорт в продажу старой позиции
|
|
2381
2329
|
const rawNewShorts = shortCands.filter((r) => {
|
|
@@ -2395,30 +2343,53 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2395
2343
|
const newBuys = buyExhaustion.ready;
|
|
2396
2344
|
const newShorts = shortExhaustion.ready;
|
|
2397
2345
|
const deferredByImpulse = [...buyExhaustion.deferred, ...shortExhaustion.deferred];
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2346
|
+
const dynSlotRub = slotRub;
|
|
2347
|
+
|
|
2348
|
+
const netPickWeight = (r) => {
|
|
2349
|
+
const rawPct = Math.abs(r.expectedPct || 0);
|
|
2350
|
+
if (!autoAdapt) return Math.max(0.01, rawPct);
|
|
2351
|
+
const roundTripCostPct = TIER1_LIQUID_SYMBOLS.has(r.symbol) ? 0.13 : 0.25;
|
|
2352
|
+
return Math.max(0.01, rawPct - roundTripCostPct);
|
|
2353
|
+
};
|
|
2354
|
+
const calcDesiredNewRub = (r, side) => {
|
|
2355
|
+
const isTier1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
|
|
2356
|
+
const posSide = side === 'short' ? 'short' : 'long';
|
|
2357
|
+
const tierMult = autoAdapt ? tierSizeMult(r.symbol, leverage, posSide) : 1;
|
|
2358
|
+
const barTurnoverRub = !isTier1 ? recentBarTurnoverRub(r.symbol) : 0;
|
|
2359
|
+
const stopPct = protectiveStopPct(posSide);
|
|
2360
|
+
const effSlotRub = (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * tierMult;
|
|
2361
|
+
const targetRub = autoAdapt
|
|
2362
|
+
? targetPositionRub({ symbol: r.symbol, side: posSide, budget: budget * tierMult, slotRub: effSlotRub, conv: convOf(r), stopPct, barTurnoverRub, profile: sizingProfile })
|
|
2363
|
+
: Math.min(symbolCapRub(r.symbol, posSide, budget * tierMult, barTurnoverRub), effSlotRub);
|
|
2364
|
+
return { targetRub, tierMult, stopPct };
|
|
2365
|
+
};
|
|
2366
|
+
|
|
2367
|
+
const combinedNew = [
|
|
2368
|
+
...newBuys.map((r) => ({ r, side: 'buy' })),
|
|
2369
|
+
...newShorts.map((r) => ({ r, side: 'short' })),
|
|
2370
|
+
];
|
|
2371
|
+
if (autoAdapt && combinedNew.length > 1) {
|
|
2372
|
+
combinedNew.sort((a, b) => netPickWeight(b.r) - netPickWeight(a.r));
|
|
2373
|
+
}
|
|
2374
|
+
const preparedNew = combinedNew.map(({ r, side }) => {
|
|
2375
|
+
const { targetRub, tierMult, stopPct } = calcDesiredNewRub(r, side);
|
|
2376
|
+
return { r, side, weight: netPickWeight(r), targetRub, tierMult, stopPct };
|
|
2377
|
+
}).filter((x) => x.targetRub > 5000);
|
|
2378
|
+
const totalDesiredNewRub = preparedNew.reduce((s, x) => s + x.targetRub, 0);
|
|
2405
2379
|
|
|
2406
2380
|
// В режиме «Плавный переход» (smooth):
|
|
2407
|
-
// • высвобождаем кэш из старого портфеля как под новые ПОКУПКИ, так и под новые
|
|
2408
|
-
// • даже если прямо сейчас нет нового входа, но свободного кэша на счёте меньше 1 рабочего слота, плавно продаём на каждом шаге по 1–2 самые слабые старые акции с отрицательным прогнозом (expectedPct <= -0.02%)
|
|
2409
|
-
// • сортируем кандидатов строго по худшему прогнозу ИИ (сначала акции с отрицательным ожиданием, среди близких — позиции в плюсе или с минимальным убытком), до 4 бумаг за шаг при активном сигнале.
|
|
2381
|
+
// • высвобождаем кэш из старого портфеля как под новые ПОКУПКИ, так и под новые ШОРТЫ;
|
|
2382
|
+
// • даже если прямо сейчас нет нового входа, но свободного кэша на счёте меньше 1 рабочего слота, плавно продаём на каждом шаге по 1–2 самые слабые старые акции с отрицательным прогнозом (expectedPct <= -0.02%).
|
|
2410
2383
|
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);
|
|
2384
|
+
const activeEntries = preparedNew.map((x) => x.r);
|
|
2413
2385
|
const hasWeakLegacy = smoothSellPool.some((x) => (x.r.expectedPct || 0) <= -0.02);
|
|
2414
2386
|
const proactiveReserveRub = (!activeEntries.length && hasWeakLegacy && availRub < slotRub) ? slotRub : 0;
|
|
2415
|
-
const targetNeedRub = Math.max(
|
|
2387
|
+
const targetNeedRub = Math.max(totalDesiredNewRub, proactiveReserveRub);
|
|
2416
2388
|
let shortfall = Math.min(Math.max(0, budget * 0.99 - usedRub), targetNeedRub) - availRub;
|
|
2417
2389
|
if (shortfall > 1000) {
|
|
2418
2390
|
smoothSellPool.sort((a, b) => {
|
|
2419
2391
|
const ea = Number(a.r.expectedPct || 0);
|
|
2420
2392
|
const eb = Number(b.r.expectedPct || 0);
|
|
2421
|
-
// Сначала бумаги с явно худшим прогнозом (разница > 0.03%), при близком прогнозе — сначала плюсовые/околонулевые
|
|
2422
2393
|
if (Math.abs(ea - eb) > 0.03) return ea - eb;
|
|
2423
2394
|
return ((b.pos.pnlRub >= 0 ? 1 : 0) - (a.pos.pnlRub >= 0 ? 1 : 0)) || (ea - eb);
|
|
2424
2395
|
});
|
|
@@ -2445,27 +2416,15 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2445
2416
|
}
|
|
2446
2417
|
}
|
|
2447
2418
|
}
|
|
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')) {
|
|
2419
|
+
|
|
2420
|
+
// Если включено «Свободные рубли в LQDT» (cashSweep) и счёт без залогового плеча (!collateral), при нехватке рублей продаём нужное число паёв LQDT
|
|
2421
|
+
if (!collateral && acc.cashSweep && acc.broker === 'tbank' && preparedNew.length > 0 && !closing.has('LQDT') && !activeOrderSymbols.has('LQDT')) {
|
|
2456
2422
|
const lqdtPos = heldMap.get('LQDT');
|
|
2457
2423
|
const lqdtAlgoQty = lqdtPos ? algoQtyOf('LQDT', lqdtPos.qty) : 0;
|
|
2458
2424
|
if (lqdtAlgoQty > 0 && lqdtPos.price > 0) {
|
|
2459
2425
|
const lqdtSp = specs.LQDT || { lotSize: 1, minStep: 0.001, decimals: 3 };
|
|
2460
2426
|
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;
|
|
2427
|
+
const needFromLqdtRub = Math.min(Math.max(0, budget * 0.99 - usedRub), totalDesiredNewRub) - availRub;
|
|
2469
2428
|
if (lqdtLots >= 1 && needFromLqdtRub > 1500) {
|
|
2470
2429
|
const limPx = sniperLimitPrice(lqdtPos.price, -1, 0.03, lqdtSp);
|
|
2471
2430
|
const lotVal = limPx * lqdtSp.lotSize;
|
|
@@ -2474,43 +2433,114 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2474
2433
|
orders.push({
|
|
2475
2434
|
symbol: 'LQDT', action: 'sell', lotSize: lqdtSp.lotSize, lots: sellLots, shares: sellLots * lqdtSp.lotSize,
|
|
2476
2435
|
refPrice: lqdtPos.price, limitPrice: limPx, amountRub,
|
|
2477
|
-
reason: `Высвобождение ${fmtRubSimple(amountRub)} ₽ из фонда денежного рынка LQDT под сигнал стратегии (${
|
|
2436
|
+
reason: `Высвобождение ${fmtRubSimple(amountRub)} ₽ из фонда денежного рынка LQDT под сигнал стратегии (${preparedNew.map((x) => x.r.symbol).join(', ')})`,
|
|
2478
2437
|
});
|
|
2479
2438
|
closing.add('LQDT');
|
|
2480
2439
|
availRub += amountRub;
|
|
2481
2440
|
}
|
|
2482
2441
|
}
|
|
2483
2442
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2443
|
+
|
|
2444
|
+
// Двухступенчатое динамическое потеснение под новый сильный сигнал 1-го эшелона (единое с server/model/simulator.js):
|
|
2445
|
+
// в режиме auto работает и на быстрых внутрисвечных входах, а в режиме confirm — на плановых 30-минутных шагах (!onlyEntries),
|
|
2446
|
+
// ротируя остывшие позиции либо частично подрезая позицию 2-го эшелона (с сохранением entryTs и peakPrice оставшихся лотов).
|
|
2447
|
+
if (autoAdapt && STRATEGY_POLICY.crowdOut !== false && (acc.mode === 'auto' || !onlyEntries) && preparedNew.length > 0) {
|
|
2448
|
+
const bestT1 = preparedNew.find((x) => TIER1_LIQUID_SYMBOLS.has(x.r.symbol));
|
|
2449
|
+
const minChunkRub = Math.max(45000, budget * (STRATEGY_POLICY.crowdMinPct ?? 0.06));
|
|
2450
|
+
let shortfall = totalDesiredNewRub - availRub;
|
|
2451
|
+
if (bestT1 && shortfall >= minChunkRub) {
|
|
2452
|
+
const minHoldMs = (STRATEGY_POLICY.crowdMinMin ?? 20) * 60000;
|
|
2453
|
+
const rotBufPct = STRATEGY_POLICY.crowdRotBufPct ?? 0.08;
|
|
2454
|
+
const trimBufPct = STRATEGY_POLICY.crowdTrimBufPct ?? 0.12;
|
|
2455
|
+
const elig = [];
|
|
2456
|
+
for (const [sym, pos] of heldMap) {
|
|
2457
|
+
if (closing.has(sym) || activeOrderSymbols.has(sym) || !rowMap.has(sym)) continue;
|
|
2458
|
+
const aQ = algoQtyOf(sym, pos.qty);
|
|
2459
|
+
if (!aQ) continue;
|
|
2460
|
+
// На безналичном счёте без маржиналки закрытие шорта тратит кэш, а не высвобождает его под покупку
|
|
2461
|
+
if (!marginSizing && aQ < 0) continue;
|
|
2462
|
+
const entryAt = Number(acc.entryTsBySymbol?.[sym]) || 0;
|
|
2463
|
+
if (entryAt > 0 && nowTs - entryAt < minHoldMs) continue;
|
|
2464
|
+
const sp = specs[sym] || { lotSize: 1, minStep: 0.01, decimals: 2 };
|
|
2465
|
+
const totalLots = Math.floor(Math.abs(aQ) / sp.lotSize);
|
|
2466
|
+
const rCur = rowMap.get(sym);
|
|
2467
|
+
const px = Number(rCur?.price || pos.price) || 0;
|
|
2468
|
+
if (totalLots < 1 || !(px > 0)) continue;
|
|
2469
|
+
const isLongPos = aQ > 0;
|
|
2470
|
+
const curSignedPct = isLongPos ? Number(rCur?.expectedPct || 0) : -Number(rCur?.expectedPct || 0);
|
|
2471
|
+
const thrEntryPct = isLongPos ? reqBuyEdgeOf(sym) : reqShortEdgeOf(sym);
|
|
2472
|
+
const heldIsT1 = TIER1_LIQUID_SYMBOLS.has(sym);
|
|
2473
|
+
elig.push({ sym, pos, rCur, sp, totalLots, px, isLongPos, curSignedPct, thrEntryPct, heldIsT1, posRub: totalLots * sp.lotSize * px, tierRank: heldIsT1 ? 1 : 0 });
|
|
2474
|
+
}
|
|
2475
|
+
elig.sort((a, b) => (a.tierRank - b.tierRank) || (a.curSignedPct - b.curSignedPct));
|
|
2476
|
+
|
|
2477
|
+
for (const el of elig) {
|
|
2478
|
+
if (shortfall < minChunkRub * 0.5) break;
|
|
2479
|
+
const coolRatio = el.heldIsT1 ? (STRATEGY_POLICY.crowdCoolRatioT1 ?? 0.45) : (STRATEGY_POLICY.crowdCoolRatioT2 ?? 0.80);
|
|
2480
|
+
const cooled = el.curSignedPct < el.thrEntryPct * coolRatio;
|
|
2481
|
+
const netAdvPct = bestT1.weight - Math.max(0, el.curSignedPct);
|
|
2482
|
+
const limPx = sniperLimitPrice(el.px, el.isLongPos ? -1 : 1, acc.limitOffsetPct, el.sp);
|
|
2483
|
+
const lotVal = limPx * el.sp.lotSize;
|
|
2484
|
+
if (!(lotVal > 0)) continue;
|
|
2485
|
+
|
|
2486
|
+
let lotsToClose = 0;
|
|
2487
|
+
let fullRotate = false;
|
|
2488
|
+
if (cooled && netAdvPct >= rotBufPct) {
|
|
2489
|
+
if (!el.heldIsT1 || el.curSignedPct < el.thrEntryPct * 0.35 || el.posRub - shortfall < minChunkRub) {
|
|
2490
|
+
lotsToClose = el.totalLots;
|
|
2491
|
+
fullRotate = true;
|
|
2492
|
+
} else {
|
|
2493
|
+
const maxTrimRub = el.posRub - minChunkRub;
|
|
2494
|
+
const trimRub = Math.min(shortfall, maxTrimRub);
|
|
2495
|
+
if (trimRub >= minChunkRub) lotsToClose = Math.min(el.totalLots - 1, Math.max(1, Math.ceil(trimRub / lotVal)));
|
|
2496
|
+
}
|
|
2497
|
+
} else if (!cooled && !el.heldIsT1 && netAdvPct >= trimBufPct && el.posRub >= minChunkRub * 1.5) {
|
|
2498
|
+
const maxTrimRub = el.posRub - minChunkRub;
|
|
2499
|
+
const trimRub = Math.min(shortfall, maxTrimRub);
|
|
2500
|
+
if (trimRub >= minChunkRub) lotsToClose = Math.min(el.totalLots - 1, Math.max(1, Math.ceil(trimRub / lotVal)));
|
|
2501
|
+
}
|
|
2502
|
+
if (lotsToClose >= 1) {
|
|
2503
|
+
const isTrim = !fullRotate && lotsToClose < el.totalLots;
|
|
2504
|
+
const amountRub = round(lotsToClose * el.sp.lotSize * limPx, 2);
|
|
2505
|
+
const freedExpRub = lotsToClose * el.sp.lotSize * el.pos.price;
|
|
2506
|
+
orders.push({
|
|
2507
|
+
symbol: el.sym,
|
|
2508
|
+
action: el.isLongPos ? 'sell' : 'cover',
|
|
2509
|
+
lotSize: el.sp.lotSize,
|
|
2510
|
+
lots: lotsToClose,
|
|
2511
|
+
shares: lotsToClose * el.sp.lotSize,
|
|
2512
|
+
refPrice: el.px,
|
|
2513
|
+
limitPrice: limPx,
|
|
2514
|
+
amountRub,
|
|
2515
|
+
isTrim,
|
|
2516
|
+
urgentExit: true,
|
|
2517
|
+
reason: isTrim
|
|
2518
|
+
? `Частичное потеснение (${lotsToClose} из ${el.totalLots} лот., прогноз ${el.rCur?.expectedPct > 0 ? '+' : ''}${el.rCur?.expectedPct}%) под сильный сигнал 1-го эшелона ${bestT1.r.symbol} (${bestT1.r.expectedPct > 0 ? '+' : ''}${bestT1.r.expectedPct}%)`
|
|
2519
|
+
: `Ротация капитала из остывшей бумаги (${el.rCur?.expectedPct > 0 ? '+' : ''}${el.rCur?.expectedPct}%) в флагман 1-го эшелона ${bestT1.r.symbol} (${bestT1.r.expectedPct > 0 ? '+' : ''}${bestT1.r.expectedPct}%)`,
|
|
2520
|
+
});
|
|
2521
|
+
if (!isTrim) {
|
|
2522
|
+
closing.add(el.sym);
|
|
2523
|
+
if (el.isLongPos) activeLongs = Math.max(0, activeLongs - 1);
|
|
2524
|
+
else activeShorts = Math.max(0, activeShorts - 1);
|
|
2525
|
+
}
|
|
2526
|
+
usedRub = Math.max(0, usedRub - freedExpRub);
|
|
2527
|
+
availRub = recomputeAvailRub();
|
|
2528
|
+
shortfall = totalDesiredNewRub - availRub;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2492
2532
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
//
|
|
2496
|
-
//
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
const
|
|
2502
|
-
remWeightSum = Math.max(0, remWeightSum - wCur);
|
|
2503
|
-
remNew--;
|
|
2533
|
+
|
|
2534
|
+
// Итеративное распределение свободного ресурса методом перелива (waterFillAllocations в policy.js):
|
|
2535
|
+
// кандидаты с узким потолком (2-й эшелон / лимит 15% от 10-минутного оборота свечи) забирают ровно свой потолок,
|
|
2536
|
+
// а весь оставшийся лимит без холостого простоя переливается флагманским лидерам 1-го эшелона.
|
|
2537
|
+
const allocSums = autoAdapt
|
|
2538
|
+
? waterFillAllocations(preparedNew, availRub)
|
|
2539
|
+
: Float64Array.from(preparedNew.map((x) => Math.min(x.targetRub, availRub / Math.max(1, preparedNew.length))));
|
|
2540
|
+
for (let idx = 0; idx < preparedNew.length; idx++) {
|
|
2541
|
+
const { r, side, targetRub, tierMult, stopPct } = preparedNew[idx];
|
|
2504
2542
|
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);
|
|
2543
|
+
const sum = Math.min(allocSums[idx], availRub);
|
|
2514
2544
|
const refPx = r.price || specs[r.symbol]?.lastPrice || 0;
|
|
2515
2545
|
const effOffsetPct = isTier1 ? Math.min(Number(acc.limitOffsetPct) || 0.05, 0.06) : Math.min(Number(acc.limitOffsetPct) || 0.04, 0.04);
|
|
2516
2546
|
const ord = calcLotOrder(r.symbol, sum, refPx, side, effOffsetPct, specs);
|
|
@@ -2524,8 +2554,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2524
2554
|
bar: fc.bar, expectedPct: r.expectedPct,
|
|
2525
2555
|
thresholdPct: side === 'buy' ? reqBuyEdgeOf(r.symbol) : reqShortEdgeOf(r.symbol),
|
|
2526
2556
|
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
|
|
2557
|
+
marketDown: Boolean(fc.marketDown), conv: round(convOf(r), 3), budgetRub: budget, targetRub: round(targetRub, 0), leverage, tierMult, collateral,
|
|
2558
|
+
stopPct, day: r.day || null, onlyEntries: Boolean(onlyEntries),
|
|
2529
2559
|
},
|
|
2530
2560
|
reason: side === 'buy'
|
|
2531
2561
|
? `Покупка (${MODELS[params.modelType]?.label || params.modelType}): ожидание ${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}% за ${params.modelHorizon} ч`
|
|
@@ -2783,7 +2813,7 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
2783
2813
|
// при узком спреде (<= 0,10% или <= 2 шагов) ставим ровно в лучшую встречную цену (bestAsk / bestBid),
|
|
2784
2814
|
// а при широком спреде встаём на середине спреда (midPx), не переплачивая стаканным роботам!
|
|
2785
2815
|
const tightExitSpread = spreadPct <= 0.10 || (bestAsk - bestBid) <= 2.1 * step;
|
|
2786
|
-
if (ord.isStopLoss && spreadPct <= 0.80) {
|
|
2816
|
+
if ((ord.isStopLoss || ord.urgentExit) && spreadPct <= (ord.isStopLoss ? 0.80 : 0.35)) {
|
|
2787
2817
|
const mktExitPx = roundToStep(isBuy ? bestAsk : bestBid, step, sp.decimals);
|
|
2788
2818
|
if (mktExitPx > 0) finalLimitPx = isBuy ? Math.max(finalLimitPx, mktExitPx) : Math.min(finalLimitPx, mktExitPx);
|
|
2789
2819
|
} else if (tightExitSpread) {
|
|
@@ -3175,10 +3205,12 @@ export async function executeBrokerOrders(id, { _lockHeld = false } = {}) {
|
|
|
3175
3205
|
delete acc.entryFailReasonBySymbol[ord.symbol];
|
|
3176
3206
|
} else if (ord.action === 'sell' || ord.action === 'cover') {
|
|
3177
3207
|
hadExits = true;
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3208
|
+
if (!ord.isTrim) {
|
|
3209
|
+
delete acc.entryTsBySymbol[ord.symbol];
|
|
3210
|
+
delete acc.peakPriceBySymbol[ord.symbol];
|
|
3211
|
+
if (ord.symbol !== 'LQDT') {
|
|
3212
|
+
acc.exitCooldownBySymbol[ord.symbol] = ord.isStopLoss ? execNowTs : (execNowTs - 30 * 60000);
|
|
3213
|
+
}
|
|
3182
3214
|
}
|
|
3183
3215
|
}
|
|
3184
3216
|
} catch (e) {
|
|
@@ -3513,8 +3545,9 @@ async function tickBrokerSchedule() {
|
|
|
3513
3545
|
const lqdtBaseQty = Number(acc.baselineQty?.LQDT || 0);
|
|
3514
3546
|
const lqdtAvailRub = lqdtPos ? Math.max(0, (lqdtPos.qty - (acc.legacyMode === 'takeover' ? 0 : Math.max(0, lqdtBaseQty))) * (lqdtPos.price || 0)) : 0;
|
|
3515
3547
|
const freeCashRub = Number(acc.lastSync?.cashRub ?? acc.maxCapitalRub ?? 0) + lqdtAvailRub;
|
|
3548
|
+
const canEnterOrCrowd = freeCashRub >= 15000 || (acc.marginEnabled && (Number(acc.maxLeverage) || 1) > 1) || hasOpenAlgoPos;
|
|
3516
3549
|
const marketActiveNow = Boolean(latest && nowTs - latest <= 25 * 60000);
|
|
3517
|
-
if (marketActiveNow && acc.mode === 'auto' && acc.token && !acc.haltedReason && !(acc.skipEdgeMinutes && sessionEdgeWarning(nowTs)) &&
|
|
3550
|
+
if (marketActiveNow && acc.mode === 'auto' && acc.token && !acc.haltedReason && !(acc.skipEdgeMinutes && sessionEdgeWarning(nowTs)) && canEnterOrCrowd && nowTs - Math.max(lastFullWallTs, lastAnyWallTs) >= 20000) {
|
|
3518
3551
|
await prepareBrokerOrders(acc.id, { onlyEntries: true }).catch(() => {});
|
|
3519
3552
|
}
|
|
3520
3553
|
}
|