neuroalpha 0.3.14 → 0.3.15
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/README.md +1 -1
- package/docs//320/221/320/240/320/236/320/232/320/225/320/240/320/253_API.md +118 -0
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -1
- package/package.json +1 -1
- package/public/app.js +151 -7
- package/public/index.html +22 -8
- package/public/styles.css +4 -2
- package/server/brokers/index.js +295 -10
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ MOEX ISS: ~100 акций MOEXBMI + IMOEX, свечи по 10 минут (буд
|
|
|
104
104
|
| `server/model/transformer-gpu.js`, `gpu-worker.html/.js` | Обучение на видеокарте: WebGL в безголовом Chrome / Edge / Chromium / Brave |
|
|
105
105
|
| `server/model/profiles.js` | Описания компаний: 20 устойчивых признаков от ИИ, «текстовая» карта |
|
|
106
106
|
| `server/news/index.js` | Новости: сбор из RSS и с Мосбиржи, разметка ИИ, фильтр для живой проверки |
|
|
107
|
-
| `server/brokers/index.js` | Брокерские
|
|
107
|
+
| `server/brokers/index.js` | Брокерские счета (Т-Банк, Финам, БКС, Алор, Цифра брокер, ITI Capital, СберИнвестиции, ВТБ, Альфа-Инвестиции + DMA/QUIK-мост); лоты и шаг цены Мосбиржи; предохранители |
|
|
108
108
|
| `server/strategist/` | Стратег: цикл агента, инструменты, аналитика |
|
|
109
109
|
| `server/llm/client.js` | Потоковый клиент модели (SSE, функции, таймаут по простою, отмена) |
|
|
110
110
|
| `server/llm/providers.js` | Реестр подключений и выбранная модель, GigaChat OAuth |
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Подключение брокерских счетов в «НейроАльфе»
|
|
2
|
+
|
|
3
|
+
Платформа «НейроАльфа» поддерживает подключение **9 российских брокеров** для работы с акциями Мосбиржи (`TQBR`). Все токены и ключи доступа шифруются алгоритмом **AES-256-GCM** и никогда не передаются обратно в интерфейс браузера.
|
|
4
|
+
|
|
5
|
+
> **Важное правило безопасности и Оферты:**
|
|
6
|
+
> Запрещается подключать к платформе счета, участвующие в сервисах **автоследования / копирования сделок** (как в роли ведущего стратегии, так и в роли подписчика). Платформа автоматически проверяет счета на признаки автоследования и блокирует торговлю при их обнаружении.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Т-Банк (`T-Invest API`) — рекомендуется
|
|
11
|
+
|
|
12
|
+
Самый быстрый и функциональный облачный API на российском рынке: поддерживает автопоиск всех ваших субсчетов, проверку маржинальной торговли (`GetMarginAttributes`), проверку доступных лотов (`GetMaxLots`), выписку операций за все годы и выставление лимитных заявок.
|
|
13
|
+
|
|
14
|
+
### Пошаговая инструкция:
|
|
15
|
+
1. Откройте веб-кабинет **Т-Инвестиций** на сайте `https://www.tbank.ru/invest/settings/` (раздел **Настройки → Токены T-Invest API**).
|
|
16
|
+
2. Нажмите **«Выпустить токен»**:
|
|
17
|
+
- Выберите права: **«Полный доступ»** (если хотите отправлять заявки из терминала или в режиме «Автомат») либо «Только чтение».
|
|
18
|
+
- Выберите счета: **«Все счета»** (или конкретный брокерский счёт).
|
|
19
|
+
3. Подтвердите выпуск токена СМС-кодом и скопируйте строку токена (начинается с `t.…`).
|
|
20
|
+
4. В терминале «НейроАльфа» откройте **«Настройки → Брокерские счета → Добавить брокерский счёт»**:
|
|
21
|
+
- Выберите брокера **«Т-Банк (T-Invest API)»**;
|
|
22
|
+
- Вставьте токен `t.…` в поле **«Токен T-Invest API»**;
|
|
23
|
+
- Нажмите кнопку **«🔍 Найти мои счета по токену»** — выберите нужный субсчёт одной кнопкой.
|
|
24
|
+
5. **Для заработка на падении акций (шорты на тарифах «Профи» и «VIP»):**
|
|
25
|
+
- В мобильном приложении Т-Инвестиций зайдите на выбранный счёт → нажмите на **шестерёнку (Настройки счёта)** в правом верхнем углу → включите тумблер **«Маржинальная торговля»**.
|
|
26
|
+
- Если маржинальная торговля выключена, платформа автоматически распознает это и будет вести счёт в безопасном режиме **«Только в рост (без шорта)»**.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Финам (`Finam Trade API`)
|
|
31
|
+
|
|
32
|
+
Официальный облачный REST API брокера «Финам» (`https://trade-api.finam.ru`).
|
|
33
|
+
|
|
34
|
+
### Пошаговая инструкция:
|
|
35
|
+
1. Войдите в личный кабинет клиента Финам (`https://edox.finam.ru` / `finam.ru`).
|
|
36
|
+
2. Перейдите в раздел **«Торговля» → «Токены доступа (Finam Trade API)»**.
|
|
37
|
+
3. Нажмите **«Создать новый токен»**, подтвердите выпуск по СМС и скопируйте токен (обычно начинается с `CA…`).
|
|
38
|
+
4. Узнайте ваш **Код клиента (`ClientId`)** для Единого счёта Мосбиржи (отображается в таблице токенов и в терминале FinamTrade, например `123456R` или `КлФ-…`).
|
|
39
|
+
5. В форме добавления счёта в «НейроАльфе» выберите **«Финам (Finam Trade API)»**, вставьте токен `CA…` и укажите `ClientId`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. БКС Мир инвестиций (`BCS Trade API`)
|
|
44
|
+
|
|
45
|
+
Облачный торговый REST API брокера БКС (`https://be.broker.ru`).
|
|
46
|
+
|
|
47
|
+
### Пошаговая инструкция:
|
|
48
|
+
1. Откройте веб-терминал или личный кабинет **«БКС Мир инвестиций»** в браузере.
|
|
49
|
+
2. Перейдите в **Профиль / Настройки → «Торговый API (BCS Trade API)»**.
|
|
50
|
+
3. Выпустите токен с правом **«Чтение и торговля»** (`trade-api-write`).
|
|
51
|
+
4. Скопируйте полученный **Refresh-токен** и вставьте его в поле «Refresh-токен БКС Trade API» в «НейроАльфе». Платформа сама обменивает его на сессионный JWT-ключ при каждом запросе.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 4. Алор Брокер (`Alor OpenAPI v2`)
|
|
56
|
+
|
|
57
|
+
Профессиональный облачный интерфейс Алор Брокера (`https://api.alor.ru`).
|
|
58
|
+
|
|
59
|
+
### Пошаговая инструкция:
|
|
60
|
+
1. Авторизуйтесь под вашей брокерской учётной записью на портале разработчика **`https://alor.dev`** (или через кабинет `my.alor.ru` → раздел **OpenAPI**).
|
|
61
|
+
2. Выпустите **Refresh Token** для боевого контура Мосбиржи.
|
|
62
|
+
3. В поле **«Фондовый портфель Мосбиржи»** укажите ваш портфель с литерой `D` (например, `D39004`).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 5. Цифра брокер (`Tradernet API v2`)
|
|
67
|
+
|
|
68
|
+
Облачный REST API платформы Tradernet («Цифра брокер» / бывш. ИК «Фридом Финанс», `https://tradernet.ru`).
|
|
69
|
+
|
|
70
|
+
### Пошаговая инструкция:
|
|
71
|
+
1. Войдите в веб-кабинет **`https://tradernet.ru`** → откройте **Настройки профиля → «API ключи»**.
|
|
72
|
+
2. Нажмите **«Создать ключи API»** — система выдаст два ключа: **Public Key** (публичный) и **Secret Key** (секретный).
|
|
73
|
+
3. В «НейроАльфе» выберите **«Цифра брокер (Tradernet API)»** и вставьте оба ключа в поле токена **через двоеточие без пробелов**:
|
|
74
|
+
`PublicKey:SecretKey`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 6. ITI Capital (`OpenAPI`)
|
|
79
|
+
|
|
80
|
+
Алго-брокер с поддержкой протокола OpenAPI (`https://api.iticapital.ru`).
|
|
81
|
+
|
|
82
|
+
### Пошаговая инструкция:
|
|
83
|
+
1. В личном кабинете клиента **ITI Capital** перейдите в раздел **«Торговые системы → OpenAPI»** и выпустите **Refresh Token**.
|
|
84
|
+
2. Вставьте Refresh-токен в «НейроАльфу» и укажите ваш фондовый портфель Мосбиржи вида `BP12345`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 7. СберИнвестиции (Сбербанк), ВТБ Мои Инвестиции и Альфа-Инвестиции
|
|
89
|
+
|
|
90
|
+
У розничных подразделений **СберИнвестиций**, **ВТБ** и **Альфа-Инвестиций** нет публичного облачного REST-токена для физлиц (алготорговля у этих брокеров предоставляется через десктопный терминал **QUIK**).
|
|
91
|
+
|
|
92
|
+
В «НейроАльфе» для клиентов **Сбера, ВТБ и Альфы** предусмотрены **два удобных режима работы**:
|
|
93
|
+
|
|
94
|
+
1. **Режим «Подтверждение заявок» через Telegram / MAX (рекомендуется, работает без QUIK):**
|
|
95
|
+
- При добавлении счёта выберите **«СберИнвестиции»** (или ВТБ / Альфа-Инвестиции), **оставьте поле адреса моста QUIK пустым**, укажите номер счёта и ваш **Лимит суммы на стратегию** (например, `100 000 ₽`).
|
|
96
|
+
- В начале каждого торгового шага платформа автоматически рассчитывает точный пакет лимитных заявок в лотах Мосбиржи под ваш баланс и присылает уведомление в **Telegram / MAX** и во вкладку **«⚡ Торговля и заявки»**.
|
|
97
|
+
- Вы выставляете указанные заявки в мобильном приложении СберИнвестиций / ВТБ / Альфы и нажимаете кнопку **«Исполнить заявки»** в терминале (или в боте) — платформа автоматически обновляет состав бумаг и остаток рублей в карточке вашего счёта.
|
|
98
|
+
2. **Автоматический режим через локальный HTTP-мост QUIK:**
|
|
99
|
+
- Если у вас запущен терминал QUIK Сбербанка / ВТБ с локальным HTTP REST-шлюзом (`QuikHttpBridge`), укажите его адрес (`http://127.0.0.1:8080` или адрес вашего сервера) в поле **«Адрес моста QUIK»** — тогда чтение портфеля и отправка лимитных заявок будут выполняться автоматически.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 8. Можно ли торговать напрямую через Мосбиржу без брокера (DMA / MOEX ISS / FIX)?
|
|
104
|
+
|
|
105
|
+
Этот вопрос часто возникает у трейдеров и корпоративных клиентов. Важно разделять **получение биржевых данных** и **исполнение сделок в стакане**:
|
|
106
|
+
|
|
107
|
+
1. **Рыночные данные и котировки «НейроАльфа» уже получает напрямую с Мосбиржи (без брокеров):**
|
|
108
|
+
- Все 10-минутные свечи 99 акций индекса широкого рынка, живые котировки `LAST / MARKETPRICE` (обновление каждые 45 секунд), размеры лотов (`LOTSIZE`), минимальный шаг цены (`MINSTEP`) и календарь дивидендов загружаются платформой **напрямую из ядра Московской биржи (`iss.moex.com` / `MOEX ISS`)**.
|
|
109
|
+
- Поэтому обучение моделей, карта рынка, живой радар прогнозов и виртуальные счета работают полностью независимо от брокеров.
|
|
110
|
+
|
|
111
|
+
2. **Почему для реальных сделок с акциями (`TQBR`) обязательно нужен лицензированный брокер:**
|
|
112
|
+
- По российскому законодательству (**ФЗ № 39-ФЗ «О рынке ценных бумаг»** и **ФЗ № 325-ФЗ «Об организованных торгах»**) прямой клиринговый допуск к фондовому рынку акций Мосбиржи (`TQBR`) имеют только профессиональные участники рынка ценных бумаг с лицензией Банка России и взносом в гарантийный фонд НКЦ.
|
|
113
|
+
- Частный инвестор или обычное юридическое лицо подаёт заявки в торговое ядро Мосбиржи через свой счёт у лицензированного брокера (деньги и акции при этом хранятся на вашем именном счёте в НРД / у брокера).
|
|
114
|
+
|
|
115
|
+
3. **Прямой доступ к ядру биржи (DMA — Direct Market Access / MOEX FIX / ASTS Bridge):**
|
|
116
|
+
- Для крупных алготрейдеров и институциональных клиентов (тариф **«VIP»**), у которых у брокера (Алор, Финам, ITI Capital, БКС) подключена услуга прямого биржевого шлюза **DMA (протоколы MOEX FIX / FAST или ASTS Bridge)**, «НейроАльфа» поддерживает подключение через локальный HTTP-мост (`http://ip:port/orders`).
|
|
117
|
+
- При этом для горизонта стратегии **2,5 часа** (с проверкой импульса каждые **30 минут** и выставлением лимитных заявок с защитным отступом `0,1%`) стандартные бесплатные облачные REST API **Т-Банка, Финама, БКС, Алора, Цифры и ITI Capital** (отклик `50–120 мс`) обеспечивают точно такую же цену исполнения без абонентской платы за выделенный FIX-шлюз.
|
|
118
|
+
|
package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md
CHANGED
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
| Обучать трансформер на видеокарте | Экспериментально: через встроенный в браузер Chrome / Edge движок. Если видеокарта недоступна, обучение идёт на процессоре. Результат может чуть отличаться в последних знаках |
|
|
141
141
|
| Учитывать новости в торговле | В виртуальной торговле не открывать сделку против важной свежей новости (сбор новостей идёт в фоне всегда) |
|
|
142
142
|
|
|
143
|
-
**Брокерские счета** (Т-Банк, Финам, Алор,
|
|
143
|
+
**Брокерские счета** (Т-Банк, Финам, БКС, Алор, Цифра брокер, ITI Capital, а также СберИнвестиции, ВТБ и Альфа-Инвестиции): добавляются во вкладке **«Настройки → Брокерские счета»** с выбором режима работы («Только чтение» / «Подтверждение заявок» / «Автомат») и жёсткими предохранителями: лимит суммы на стратегию (₽), дневной стоп по просадке (%), защитный отступ лимитной заявки (%) и пропуск первых и последних 10 минут сессии. Рыночные данные (свечи, живые котировки, лоты и дивиденды) платформа получает напрямую с Мосбиржи (`MOEX ISS`), а выставление реальных заявок в стакан `TQBR` по закону (ФЗ № 39-ФЗ) проходит через ваш брокерский счёт (по облачному REST API брокера, через подтверждение в Telegram/MAX или через прямой DMA/QUIK-мост). Подробная инструкция по каждому брокеру открывается прямо в форме добавления счёта и во вкладке справки **«Подключение брокеров (API)»**.
|
|
144
144
|
|
|
145
145
|
## Запуск
|
|
146
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/public/app.js
CHANGED
|
@@ -978,16 +978,17 @@ async function loadForecast(force, silent = false) {
|
|
|
978
978
|
if (rd < 0) return ` <span class="neg small" title="Опустилась на ${Math.abs(rd)} поз. в рейтинге">↓${Math.abs(rd)}</span>`;
|
|
979
979
|
return '';
|
|
980
980
|
};
|
|
981
|
+
const colgroup = `<colgroup><col style="width:34%"><col style="width:12%"><col style="width:12%"><col style="width:12%"><col style="width:10%"><col style="width:20%"></colgroup>`;
|
|
981
982
|
const row = (r) => `<tr>
|
|
982
983
|
<td><b>${r.symbol}</b> <span class="muted small">${esc(r.name)}</span>${rankBadge(r)}${dot(r.news)}</td>
|
|
983
984
|
<td class="num" style="white-space:nowrap">${r.price ? `${fmtNum(r.price, 2)} ₽` : '—'}</td>
|
|
984
985
|
<td class="num ${cls(r.expectedPct)}" style="white-space:nowrap"><b>${fmtPct(r.expectedPct, 2)}</b></td>
|
|
985
986
|
<td class="num" style="white-space:nowrap">${arrowOf(r)}</td>
|
|
986
|
-
<td>${r.signal ? '<span class="tag ok">купить</span>' : r.shortSignal ? '<span class="tag no">шорт</span>' : ''}</td>
|
|
987
|
-
<td class="muted small">${(r.influences || []).slice(0, 3).map((x) => x.symbol).join(', ')}</td>
|
|
987
|
+
<td style="text-align:center;white-space:nowrap">${r.signal ? '<span class="tag ok">купить</span>' : r.shortSignal ? '<span class="tag no">шорт</span>' : '<span class="muted small">—</span>'}</td>
|
|
988
|
+
<td class="muted small" style="white-space:nowrap">${(r.influences || []).slice(0, 3).map((x) => x.symbol).join(', ')}</td>
|
|
988
989
|
</tr>`;
|
|
989
|
-
box.innerHTML = `<table
|
|
990
|
-
<table style="margin-top:
|
|
990
|
+
box.innerHTML = `<table style="table-layout:fixed">${colgroup}<thead><tr><th>Сильнее рынка</th><th class="num">Цена</th><th class="num">Ожидание</th><th class="num">Динамика</th><th style="text-align:center">Сигнал</th><th>Под влиянием</th></tr></thead><tbody>${f.rows.slice(0, 6).map(row).join('')}</tbody></table>
|
|
991
|
+
<table style="margin-top:14px;table-layout:fixed">${colgroup}<thead><tr><th>Слабее рынка</th><th class="num">Цена</th><th class="num">Ожидание</th><th class="num">Динамика</th><th style="text-align:center">Сигнал</th><th>Под влиянием</th></tr></thead><tbody>${f.rows.slice(-4).reverse().map(row).join('')}</tbody></table>
|
|
991
992
|
<p class="muted small">Ожидание — движение относительно рынка в целом (авто-пересчёт каждые 45 сек по живым ценам Мосбиржи). «Купить» — ожидание выше порога ${fmtPct(f.thresholdPct, 2)}.${f.direction && f.direction !== 'long' ? ' «Шорт» — ожидание ниже −порога.' : ''}</p>`;
|
|
992
993
|
setupMapSlider();
|
|
993
994
|
if (state.brokers?.accounts?.length) renderBrokers();
|
|
@@ -2181,7 +2182,130 @@ addForm.addEventListener('submit', async (e) => {
|
|
|
2181
2182
|
// ======================================================================
|
|
2182
2183
|
// БРОКЕРСКИЕ СЧЕТА (Т-БАНК, ФИНАМ, АЛОР, БКС)
|
|
2183
2184
|
// ======================================================================
|
|
2184
|
-
const BROKER_LABELS = {
|
|
2185
|
+
const BROKER_LABELS = {
|
|
2186
|
+
tbank: 'Т-Банк',
|
|
2187
|
+
finam: 'Финам',
|
|
2188
|
+
bcs: 'БКС',
|
|
2189
|
+
alor: 'Алор',
|
|
2190
|
+
cifra: 'Цифра брокер',
|
|
2191
|
+
iticapital: 'ITI Capital',
|
|
2192
|
+
sber: 'СберИнвестиции',
|
|
2193
|
+
vtb: 'ВТБ Инвестиции',
|
|
2194
|
+
alfa: 'Альфа-Инвестиции',
|
|
2195
|
+
};
|
|
2196
|
+
const BROKER_GUIDES = {
|
|
2197
|
+
tbank: {
|
|
2198
|
+
tokenLbl: 'Токен T-Invest API',
|
|
2199
|
+
tokenPh: 't.… (токен Т-Инвестиций)',
|
|
2200
|
+
accLbl: 'ID субсчёта Т-Банка',
|
|
2201
|
+
accPh: 'ID счёта (или нажмите кнопку справа →)',
|
|
2202
|
+
canDiscover: true,
|
|
2203
|
+
summary: '📖 Как получить токен T-Invest API в Т-Банке (за 1 минуту)',
|
|
2204
|
+
html: `<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2205
|
+
<li>Откройте веб-кабинет <b>Т-Инвестиций</b> на сайте <code>tbank.ru/invest/settings</code> (раздел <b>Настройки → Токены T-Invest API</b>).</li>
|
|
2206
|
+
<li>Нажмите <b>«Выпустить токен»</b>, выберите права: <b>«Полный доступ»</b> (для выставления заявок) или «Только чтение», и укажите <b>«Все счета»</b>.</li>
|
|
2207
|
+
<li>Скопируйте полученный токен (начинается на <code>t.…</code>) в поле «Токен T-Invest API» ниже и нажмите кнопку <b>«🔍 Найти мои счета по токену»</b> — платформа сама покажет все ваши субсчета, их баланс и статус маржинальной торговли.</li>
|
|
2208
|
+
<li><b>Для работы с шортами (тарифы Профи и VIP):</b> в мобильном приложении Т-Инвестиций зайдите в настройки выбранного счёта (шестерёнка в правом верхнем углу счёта) и включите тумблер <b>«Маржинальная торговля»</b>.</li>
|
|
2209
|
+
</ol>`,
|
|
2210
|
+
},
|
|
2211
|
+
finam: {
|
|
2212
|
+
tokenLbl: 'Токен Finam Trade API',
|
|
2213
|
+
tokenPh: 'CA… (токен доступа Finam Trade API)',
|
|
2214
|
+
accLbl: 'Код клиента (ClientId)',
|
|
2215
|
+
accPh: 'Например, 123456R (Единый счёт Мосбиржи)',
|
|
2216
|
+
canDiscover: false,
|
|
2217
|
+
summary: '📖 Как получить токен Trade API и ClientId в Финаме',
|
|
2218
|
+
html: `<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2219
|
+
<li>Войдите в личный кабинет клиента Финам (<code>edox.finam.ru</code> / <code>finam.ru</code>) → раздел <b>«Торговля» → «Токены доступа (Finam Trade API)»</b>.</li>
|
|
2220
|
+
<li>Нажмите <b>«Создать новый токен»</b>, подтвердите выпуск по СМС и скопируйте строку токена (начинается на <code>CA…</code>) в поле «Токен Finam Trade API».</li>
|
|
2221
|
+
<li>В поле <b>«Код клиента (ClientId)»</b> укажите торговый код вашего фондового счёта Мосбиржи (обычно вида <code>123456R</code> или <code>КлФ-…</code>, он отображается рядом с токеном и в терминале FinamTrade).</li>
|
|
2222
|
+
</ol>`,
|
|
2223
|
+
},
|
|
2224
|
+
bcs: {
|
|
2225
|
+
tokenLbl: 'Refresh-токен БКС Trade API',
|
|
2226
|
+
tokenPh: 'Refresh-токен из веб-терминала БКС Мир инвестиций',
|
|
2227
|
+
accLbl: 'Номер соглашения / субсчёта',
|
|
2228
|
+
accPh: 'Оставьте пустым для основного счёта (или укажите номер соглашения)',
|
|
2229
|
+
canDiscover: false,
|
|
2230
|
+
summary: '📖 Как получить Refresh-токен в БКС Мир инвестиций',
|
|
2231
|
+
html: `<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2232
|
+
<li>Откройте веб-терминал <b>«БКС Мир инвестиций»</b> в браузере → перейдите в <b>Профиль / Настройки → «Торговый API (BCS Trade API)»</b>.</li>
|
|
2233
|
+
<li>Выпустите новый токен с правом <b>«Чтение и торговля»</b> (<code>trade-api-write</code>).</li>
|
|
2234
|
+
<li>Скопируйте полученный <b>Refresh-токен</b> в поле ниже. Платформа автоматически обменивает его на короткоживущий JWT-токен при каждом обращении.</li>
|
|
2235
|
+
</ol>`,
|
|
2236
|
+
},
|
|
2237
|
+
alor: {
|
|
2238
|
+
tokenLbl: 'Refresh-токен Alor OpenAPI',
|
|
2239
|
+
tokenPh: 'Refresh-токен с портала alor.dev',
|
|
2240
|
+
accLbl: 'Фондовый портфель Мосбиржи',
|
|
2241
|
+
accPh: 'Например, D12345',
|
|
2242
|
+
canDiscover: false,
|
|
2243
|
+
summary: '📖 Как получить Refresh-токен и номер портфеля в Алор Брокере',
|
|
2244
|
+
html: `<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2245
|
+
<li>Авторизуйтесь под учётной записью клиента Алор на портале разработчика <code>alor.dev</code> (или в личном кабинете <code>my.alor.ru</code> → раздел <b>OpenAPI</b>).</li>
|
|
2246
|
+
<li>Нажмите <b>«Выпустить Refresh Token»</b> для боевого контура (или контура Разработки, если включён тумблер «Песочница»).</li>
|
|
2247
|
+
<li>В поле <b>«Фондовый портфель Мосбиржи»</b> укажите ваш фондовый портфель с литерой <code>D</code> (например, <code>D39004</code>).</li>
|
|
2248
|
+
</ol>`,
|
|
2249
|
+
},
|
|
2250
|
+
cifra: {
|
|
2251
|
+
tokenLbl: 'Ключи Tradernet (Public:Secret)',
|
|
2252
|
+
tokenPh: 'PublicKey:SecretKey (через двоеточие)',
|
|
2253
|
+
accLbl: 'Номер счёта Tradernet',
|
|
2254
|
+
accPh: 'Необязательно (определяется автоматически по ключу)',
|
|
2255
|
+
canDiscover: false,
|
|
2256
|
+
summary: '📖 Как получить API-ключи Tradernet в «Цифра брокер» (Freedom24)',
|
|
2257
|
+
html: `<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2258
|
+
<li>Войдите в веб-терминал <b>Tradernet («Цифра брокер»)</b> на сайте <code>tradernet.ru</code> → откройте <b>Настройки профиля → «API ключи»</b>.</li>
|
|
2259
|
+
<li>Сгенерируйте пару ключей: <b>Публичный ключ (Public Key)</b> и <b>Секретный ключ (Secret Key)</b>.</li>
|
|
2260
|
+
<li>Вставьте их в поле ниже <b>через двоеточие без пробелов</b>: <code>ВашPublicKey:ВашSecretKey</code>. Платформа подписывает каждый запрос алгоритмом HMAC-SHA256.</li>
|
|
2261
|
+
</ol>`,
|
|
2262
|
+
},
|
|
2263
|
+
iticapital: {
|
|
2264
|
+
tokenLbl: 'Refresh-токен ITI Capital',
|
|
2265
|
+
tokenPh: 'Refresh-токен OpenAPI ITI Capital',
|
|
2266
|
+
accLbl: 'Код портфеля Мосбиржи',
|
|
2267
|
+
accPh: 'Например, BP12345',
|
|
2268
|
+
canDiscover: false,
|
|
2269
|
+
summary: '📖 Как подключить счёт ITI Capital по OpenAPI',
|
|
2270
|
+
html: `<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2271
|
+
<li>В личном кабинете <b>ITI Capital</b> откройте раздел <b>Торговые системы → OpenAPI</b> и выпустите <b>Refresh Token</b>.</li>
|
|
2272
|
+
<li>Скопируйте токен в поле ниже и укажите ваш фондовый портфель Мосбиржи вида <code>BP12345</code>.</li>
|
|
2273
|
+
</ol>`,
|
|
2274
|
+
},
|
|
2275
|
+
sber: {
|
|
2276
|
+
tokenLbl: 'Адрес моста QUIK (необязательно)',
|
|
2277
|
+
tokenPh: 'Оставьте пустым для режима подтверждения (или укажите http://127.0.0.1:8080)',
|
|
2278
|
+
accLbl: 'Номер счёта СберИнвестиций',
|
|
2279
|
+
accPh: 'Например, 400ABCD (для отображения и учёта)',
|
|
2280
|
+
canDiscover: false,
|
|
2281
|
+
summary: '📖 Особенности подключения «СберИнвестиций» (Сбербанк)',
|
|
2282
|
+
html: `<div style="display:grid;gap:6px">
|
|
2283
|
+
<div>У розничного брокера <b>«СберИнвестиции»</b> нет прямого облачного REST-токена для физлиц (алготорговля в Сбере предоставляется через терминал <b>QUIK</b>). В «НейроАльфе» доступны <b>2 режима работы со счётом Сбера</b>:</div>
|
|
2284
|
+
<ol style="margin:0;padding-left:18px;display:grid;gap:4px">
|
|
2285
|
+
<li><b>Режим «Подтверждение заявок» через Telegram / MAX (рекомендуется, без настройки QUIK):</b> оставьте поле «Адрес моста QUIK» <b>пустым</b>, укажите ваш лимит суммы на стратегию и сохраните счёт. Платформа будет рассчитывать точные лимитные заявки в лотах Мосбиржи под ваш баланс и присылать их в Telegram / MAX и во вкладку «Торговля и заявки». После выставления заявки в приложении СберИнвестиций нажмите <b>«Исполнить заявки»</b> — платформа сама учтёт купленные бумаги и остаток кэша в портфеле.</li>
|
|
2286
|
+
<li><b>Автоматический режим через HTTP-мост QUIK:</b> если у вас установлен терминал QUIK Сбербанка с запущенным локальным HTTP REST-шлюзом (<code>QuikHttpBridge</code>), укажите его адрес (например, <code>http://ваш_ip:8080</code>) в поле выше.</li>
|
|
2287
|
+
</ol>
|
|
2288
|
+
</div>`,
|
|
2289
|
+
},
|
|
2290
|
+
vtb: {
|
|
2291
|
+
tokenLbl: 'Адрес моста QUIK (необязательно)',
|
|
2292
|
+
tokenPh: 'Оставьте пустым для режима подтверждения (или укажите http://…)',
|
|
2293
|
+
accLbl: 'Номер счёта ВТБ Инвестиций',
|
|
2294
|
+
accPh: 'Например, номер субсчёта ВТБ',
|
|
2295
|
+
canDiscover: false,
|
|
2296
|
+
summary: '📖 Особенности подключения «ВТБ Мои Инвестиции»',
|
|
2297
|
+
html: `<div>У <b>«ВТБ Мои Инвестиции»</b> нет облачного REST-токена для физлиц (доступен только терминал QUIK). Оставьте поле адреса QUIK <b>пустым</b>, чтобы работать в режиме <b>«Подтверждение заявок»</b> (расчёт точных лотов Мосбиржи с уведомлениями в Telegram / MAX и автоматическим учётом позиций после подтверждения), либо укажите URL вашего HTTP-шлюза QUIK.</div>`,
|
|
2298
|
+
},
|
|
2299
|
+
alfa: {
|
|
2300
|
+
tokenLbl: 'Адрес моста QUIK (необязательно)',
|
|
2301
|
+
tokenPh: 'Оставьте пустым для режима подтверждения (или укажите http://…)',
|
|
2302
|
+
accLbl: 'Номер счёта Альфа-Инвестиций',
|
|
2303
|
+
accPh: 'Например, номер счёта Альфа-Инвестиций',
|
|
2304
|
+
canDiscover: false,
|
|
2305
|
+
summary: '📖 Особенности подключения «Альфа-Инвестиций»',
|
|
2306
|
+
html: `<div>У <b>«Альфа-Инвестиций»</b> для физлиц нет облачного REST-токена. Оставьте поле адреса <b>пустым</b> для работы в режиме <b>«Подтверждение заявок»</b> (точные лимитные заявки в лотах с пушами в Telegram / MAX и ведением портфеля внутри платформы) или укажите адрес вашего торгового моста.</div>`,
|
|
2307
|
+
},
|
|
2308
|
+
};
|
|
2185
2309
|
const BROKER_MODES = { readonly: 'Только чтение', confirm: 'Подтверждение заявок', auto: 'Автомат' };
|
|
2186
2310
|
const LEGACY_MODES = {
|
|
2187
2311
|
isolate: 'Изолированный карман (старые бумаги не трогаются)',
|
|
@@ -2673,12 +2797,26 @@ function getBrokerFormSnap() {
|
|
|
2673
2797
|
});
|
|
2674
2798
|
}
|
|
2675
2799
|
|
|
2800
|
+
function syncBrokerGuideUi() {
|
|
2801
|
+
const f = brokerForm?.elements;
|
|
2802
|
+
if (!f) return;
|
|
2803
|
+
const g = BROKER_GUIDES[f.broker.value] || BROKER_GUIDES.tbank;
|
|
2804
|
+
if ($('#broker-token-lbl')) $('#broker-token-lbl').textContent = g.tokenLbl;
|
|
2805
|
+
if ($('#broker-token-input')) $('#broker-token-input').placeholder = editingBrokerId ? 'не меняется — оставьте пустым' : g.tokenPh;
|
|
2806
|
+
if ($('#broker-acc-lbl')) $('#broker-acc-lbl').textContent = g.accLbl;
|
|
2807
|
+
if ($('#broker-acc-input')) $('#broker-acc-input').placeholder = g.accPh;
|
|
2808
|
+
if ($('#btn-broker-discover')) $('#btn-broker-discover').hidden = !g.canDiscover;
|
|
2809
|
+
if ($('#broker-guide-sum')) $('#broker-guide-sum').textContent = g.summary;
|
|
2810
|
+
if ($('#broker-guide-body')) $('#broker-guide-body').innerHTML = g.html;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2676
2813
|
function updateBrokerSaveBtnState() {
|
|
2677
2814
|
const btn = $('#broker-save-btn');
|
|
2678
2815
|
if (!btn) return;
|
|
2679
2816
|
if (!editingBrokerId) {
|
|
2680
2817
|
const f = brokerForm.elements;
|
|
2681
|
-
const
|
|
2818
|
+
const isManualCapable = ['sber', 'vtb', 'alfa'].includes(f.broker.value);
|
|
2819
|
+
const hasNewInput = Boolean(f.token.value.trim() || f.accountId.value.trim() || f.name.value.trim() || isManualCapable);
|
|
2682
2820
|
btn.disabled = !hasNewInput;
|
|
2683
2821
|
btn.classList.toggle('primary', hasNewInput);
|
|
2684
2822
|
btn.title = hasNewInput ? '' : 'Заполните токен или данные счёта';
|
|
@@ -2691,7 +2829,10 @@ function updateBrokerSaveBtnState() {
|
|
|
2691
2829
|
}
|
|
2692
2830
|
|
|
2693
2831
|
brokerForm.addEventListener('input', updateBrokerSaveBtnState);
|
|
2694
|
-
brokerForm.addEventListener('change',
|
|
2832
|
+
brokerForm.addEventListener('change', () => {
|
|
2833
|
+
syncBrokerGuideUi();
|
|
2834
|
+
updateBrokerSaveBtnState();
|
|
2835
|
+
});
|
|
2695
2836
|
|
|
2696
2837
|
function startBrokerEdit(acc) {
|
|
2697
2838
|
editingBrokerId = acc.id;
|
|
@@ -2720,6 +2861,7 @@ function startBrokerEdit(acc) {
|
|
|
2720
2861
|
$('#broker-add-sum').textContent = `Изменить счёт «${acc.name}»`;
|
|
2721
2862
|
$('#broker-save-btn').textContent = 'Сохранить изменения';
|
|
2722
2863
|
$('#broker-cancel-btn').hidden = false;
|
|
2864
|
+
syncBrokerGuideUi();
|
|
2723
2865
|
brokerInitialSnap = getBrokerFormSnap();
|
|
2724
2866
|
updateBrokerSaveBtnState();
|
|
2725
2867
|
}
|
|
@@ -2732,10 +2874,12 @@ function stopBrokerEdit() {
|
|
|
2732
2874
|
$('#broker-add-sum').textContent = 'Добавить брокерский счёт';
|
|
2733
2875
|
$('#broker-save-btn').textContent = 'Сохранить счёт';
|
|
2734
2876
|
$('#broker-cancel-btn').hidden = true;
|
|
2877
|
+
syncBrokerGuideUi();
|
|
2735
2878
|
brokerInitialSnap = getBrokerFormSnap();
|
|
2736
2879
|
updateBrokerSaveBtnState();
|
|
2737
2880
|
}
|
|
2738
2881
|
$('#broker-cancel-btn').addEventListener('click', stopBrokerEdit);
|
|
2882
|
+
syncBrokerGuideUi();
|
|
2739
2883
|
updateBrokerSaveBtnState();
|
|
2740
2884
|
|
|
2741
2885
|
// Авто-поиск всех субсчетов в Т-Инвестициях по токену
|
package/public/index.html
CHANGED
|
@@ -253,18 +253,31 @@
|
|
|
253
253
|
<input type="hidden" name="id">
|
|
254
254
|
<label><span>Название счёта</span><input name="name" placeholder="Например, Т-Банк основной"></label>
|
|
255
255
|
<label><span>Брокер</span>
|
|
256
|
-
<select name="broker">
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
256
|
+
<select name="broker" id="broker-select">
|
|
257
|
+
<optgroup label="Облачный REST API (прямое подключение по токену)">
|
|
258
|
+
<option value="tbank">Т-Банк (T-Invest API)</option>
|
|
259
|
+
<option value="finam">Финам (Finam Trade API)</option>
|
|
260
|
+
<option value="bcs">БКС Мир инвестиций (Trade API)</option>
|
|
261
|
+
<option value="alor">Алор Брокер (Alor OpenAPI v2)</option>
|
|
262
|
+
<option value="cifra">Цифра брокер (Tradernet API)</option>
|
|
263
|
+
<option value="iticapital">ITI Capital (OpenAPI)</option>
|
|
264
|
+
</optgroup>
|
|
265
|
+
<optgroup label="Подтверждение заявок (Telegram / MAX) или мост QUIK">
|
|
266
|
+
<option value="sber">СберИнвестиции (Сбербанк)</option>
|
|
267
|
+
<option value="vtb">ВТБ Мои Инвестиции</option>
|
|
268
|
+
<option value="alfa">Альфа-Инвестиции</option>
|
|
269
|
+
</optgroup>
|
|
261
270
|
</select>
|
|
262
271
|
</label>
|
|
263
|
-
<
|
|
272
|
+
<details id="broker-guide-box" style="margin:2px 0 4px;border:1px solid var(--line);border-radius:8px;background:var(--bg);padding:8px 14px">
|
|
273
|
+
<summary id="broker-guide-sum" style="cursor:pointer;font-weight:600;font-size:13px;color:var(--accent)">📖 Как получить токен и подключить счёт (пошаговая памятка)</summary>
|
|
274
|
+
<div id="broker-guide-body" class="small" style="margin-top:8px;line-height:1.55;color:var(--text)"></div>
|
|
275
|
+
</details>
|
|
276
|
+
<label><span id="broker-token-lbl">Токен API</span><input name="token" id="broker-token-input" type="text" class="secret-input" autocomplete="off" spellcheck="false" data-lpignore="true" data-1p-ignore="true" placeholder="Токен доступа к API брокера (t.…)"></label>
|
|
264
277
|
<label>
|
|
265
|
-
<span>Номер субсчёта / портфеля</span>
|
|
278
|
+
<span id="broker-acc-lbl">Номер субсчёта / портфеля</span>
|
|
266
279
|
<div class="row-gap" style="width:100%">
|
|
267
|
-
<input name="accountId" autocomplete="off" placeholder="ID счёта (или нажмите кнопку справа →)" style="flex:1">
|
|
280
|
+
<input name="accountId" id="broker-acc-input" autocomplete="off" placeholder="ID счёта (или нажмите кнопку справа →)" style="flex:1">
|
|
268
281
|
<button type="button" id="btn-broker-discover" style="white-space:nowrap">🔍 Найти мои счета по токену</button>
|
|
269
282
|
</div>
|
|
270
283
|
</label>
|
|
@@ -353,6 +366,7 @@
|
|
|
353
366
|
<div class="seg" id="help-tabs" style="margin:0">
|
|
354
367
|
<button type="button" data-hdoc="ИНСТРУКЦИЯ.md" class="active">Как пользоваться</button>
|
|
355
368
|
<button type="button" data-hdoc="МЕХАНИКА_И_ПАРАМЕТРЫ.md">Механика и параметры</button>
|
|
369
|
+
<button type="button" data-hdoc="БРОКЕРЫ_API.md">Подключение брокеров (API)</button>
|
|
356
370
|
</div>
|
|
357
371
|
</div>
|
|
358
372
|
<button id="help-close" class="icon">✕</button>
|
package/public/styles.css
CHANGED
|
@@ -254,9 +254,11 @@ tr.best td { background: var(--best-bg); }
|
|
|
254
254
|
.stab-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
255
255
|
.stab-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
256
256
|
.stab-chip { background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; font-size: 12px; }
|
|
257
|
-
.live-card { background: var(--bg); border: 1px solid var(--line); border-radius:
|
|
257
|
+
.live-card { background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 14px 18px; margin-top: 10px; }
|
|
258
258
|
.live-card.stopped { opacity: .75; }
|
|
259
|
-
.live-card.collapsed { padding:
|
|
259
|
+
.live-card.collapsed { padding: 12px 18px; }
|
|
260
|
+
.llm-prov.live-card { padding: 14px 18px; border-bottom: 1px solid var(--line); }
|
|
261
|
+
.llm-prov.live-card.collapsed { padding: 12px 18px; }
|
|
260
262
|
.acc-fold-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; cursor: pointer; user-select: none; }
|
|
261
263
|
.acc-fold-head:hover b { color: var(--accent); }
|
|
262
264
|
.acc-caret { display: inline-block; width: 16px; color: var(--muted); font-size: 12px; transition: transform .15s; }
|
package/server/brokers/index.js
CHANGED
|
@@ -14,10 +14,60 @@ import { round, MSK, DAY } from '../util.js';
|
|
|
14
14
|
import { hub } from '../hub.js';
|
|
15
15
|
|
|
16
16
|
export const BROKER_META = [
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
{
|
|
18
|
+
id: 'tbank',
|
|
19
|
+
name: 'Т-Банк (T-Invest API)',
|
|
20
|
+
accountHint: 'ID счёта (или нажмите «Найти мои счета по токену»)',
|
|
21
|
+
tokenHint: 'Токен T-Invest API (t.…)',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'finam',
|
|
25
|
+
name: 'Финам (Finam Trade API)',
|
|
26
|
+
accountHint: 'Код клиента (ClientId, например 123456R)',
|
|
27
|
+
tokenHint: 'Токен Finam Trade API (CA…)',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'bcs',
|
|
31
|
+
name: 'БКС Мир инвестиций (Trade API)',
|
|
32
|
+
accountHint: 'Номер субсчёта / соглашения (необязательно)',
|
|
33
|
+
tokenHint: 'Refresh-токен БКС Trade API',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'alor',
|
|
37
|
+
name: 'Алор Брокер (Alor OpenAPI v2)',
|
|
38
|
+
accountHint: 'Фондовый портфель Мосбиржи (например D12345)',
|
|
39
|
+
tokenHint: 'Refresh-токен Alor OpenAPI',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'cifra',
|
|
43
|
+
name: 'Цифра брокер (Tradernet API)',
|
|
44
|
+
accountHint: 'Номер счёта Tradernet (необязательно)',
|
|
45
|
+
tokenHint: 'Ключи Tradernet в формате PublicKey:SecretKey',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'iticapital',
|
|
49
|
+
name: 'ITI Capital (OpenAPI)',
|
|
50
|
+
accountHint: 'Код портфеля Мосбиржи (например BP12345)',
|
|
51
|
+
tokenHint: 'Refresh-токен ITI Capital OpenAPI',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'sber',
|
|
55
|
+
name: 'СберИнвестиции (Сбербанк)',
|
|
56
|
+
accountHint: 'Номер брокерского счёта (например 400ABCD)',
|
|
57
|
+
tokenHint: 'URL моста QUIK (http://…) или оставьте пустым для режима подтверждения заявок',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'vtb',
|
|
61
|
+
name: 'ВТБ Мои Инвестиции',
|
|
62
|
+
accountHint: 'Номер брокерского счёта / субсчёта ВТБ',
|
|
63
|
+
tokenHint: 'URL моста QUIK (http://…) или оставьте пустым для режима подтверждения заявок',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'alfa',
|
|
67
|
+
name: 'Альфа-Инвестиции',
|
|
68
|
+
accountHint: 'Номер брокерского счёта Альфа-Инвестиций',
|
|
69
|
+
tokenHint: 'URL моста QUIK (http://…) или оставьте пустым для режима подтверждения заявок',
|
|
70
|
+
},
|
|
21
71
|
];
|
|
22
72
|
|
|
23
73
|
// ---------- справочник лотов и шага цены Мосбиржи (TQBR) ----------
|
|
@@ -176,7 +226,8 @@ export function saveBrokerAccount(input = {}) {
|
|
|
176
226
|
const st = brokersState();
|
|
177
227
|
hub.emit({ type: 'brokers:update', state: st });
|
|
178
228
|
// При изменении стратегии, лимита капитала или режима счёта сразу пересинхронизируем счёт и пересчитываем заявки под новые настройки
|
|
179
|
-
|
|
229
|
+
const canAutoInit = Boolean(token) || ['sber', 'vtb', 'alfa'].includes(broker);
|
|
230
|
+
if (strategyOrCapChanged && canAutoInit) {
|
|
180
231
|
const activeUser = currentUser();
|
|
181
232
|
setImmediate(() => {
|
|
182
233
|
runWithUser(activeUser, async () => {
|
|
@@ -312,6 +363,36 @@ async function resolveTbankMetaMap(base, headers, keys = []) {
|
|
|
312
363
|
return cache;
|
|
313
364
|
}
|
|
314
365
|
|
|
366
|
+
// Загружает и кэширует на 12 часов справочник доступности шортов по тикерам в Т-Банке (shortEnabledFlag)
|
|
367
|
+
async function ensureTbankShortableMap(acc) {
|
|
368
|
+
if (!acc || acc.broker !== 'tbank' || !acc.token) return null;
|
|
369
|
+
const cached = kvGet('tbankShortableMap');
|
|
370
|
+
if (cached?.map && Date.now() - cached.at < 12 * 3600000) return cached.map;
|
|
371
|
+
try {
|
|
372
|
+
const base = acc.sandbox ? 'https://sandbox-invest-public-api.tinkoff.ru/rest' : 'https://invest-public-api.tinkoff.ru/rest';
|
|
373
|
+
const headers = { Authorization: `Bearer ${acc.token}`, 'Content-Type': 'application/json' };
|
|
374
|
+
const r = await brokerFetch(`${base}/tinkoff.public.invest.api.contract.v1.InstrumentsService/Shares`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers,
|
|
377
|
+
body: JSON.stringify({ instrumentStatus: 'INSTRUMENT_STATUS_BASE' }),
|
|
378
|
+
timeoutMs: 15000,
|
|
379
|
+
});
|
|
380
|
+
if (!r.ok) return cached?.map || null;
|
|
381
|
+
const j = await r.json();
|
|
382
|
+
const map = {};
|
|
383
|
+
for (const inst of j.instruments || []) {
|
|
384
|
+
if (inst.ticker && inst.classCode === 'TQBR') {
|
|
385
|
+
map[inst.ticker] = Boolean(inst.shortEnabledFlag);
|
|
386
|
+
if (inst.uid || inst.figi) tbankUidCache.set(inst.ticker, inst.uid || inst.figi);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
kvSet('tbankShortableMap', { at: Date.now(), map });
|
|
390
|
+
return map;
|
|
391
|
+
} catch {
|
|
392
|
+
return cached?.map || null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
315
396
|
async function resolveTbankInstrumentId(base, headers, symbol) {
|
|
316
397
|
if (tbankUidCache.has(symbol)) return tbankUidCache.get(symbol);
|
|
317
398
|
try {
|
|
@@ -667,11 +748,109 @@ async function syncBcs(acc) {
|
|
|
667
748
|
return { accountId: acc.accountId || 'основной', totalRub: round(totalRub, 2), cashRub: round(cashRub, 2), positions };
|
|
668
749
|
}
|
|
669
750
|
|
|
751
|
+
async function syncCifra(acc) {
|
|
752
|
+
const [apiKey, secKey] = String(acc.token || '').split(':').map((s) => s.trim());
|
|
753
|
+
if (!apiKey || !secKey) throw new Error('Для Цифра брокер (Tradernet API) укажите ключи в формате PublicKey:SecretKey');
|
|
754
|
+
const qObj = { cmd: 'getPositionJson', params: {} };
|
|
755
|
+
const qStr = `q=${JSON.stringify(qObj)}`;
|
|
756
|
+
const sig = crypto.createHmac('sha256', secKey).update(qStr).digest('hex');
|
|
757
|
+
const r = await fetch('https://tradernet.ru/api/v2/cmd/getPositionJson', {
|
|
758
|
+
method: 'POST',
|
|
759
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-NtApi-PublicKey': apiKey, 'X-NtApi-Sig': sig },
|
|
760
|
+
body: qStr,
|
|
761
|
+
signal: AbortSignal.timeout(15000),
|
|
762
|
+
});
|
|
763
|
+
if (!r.ok) throw new Error(`Tradernet API: HTTP ${r.status}`);
|
|
764
|
+
const j = await r.json();
|
|
765
|
+
const ps = j?.result?.ps || {};
|
|
766
|
+
const accList = Array.isArray(ps.acc) ? ps.acc : [];
|
|
767
|
+
const rubAcc = accList.find((c) => c.curr === 'RUR' || c.curr === 'RUB') || accList[0] || {};
|
|
768
|
+
const cashRub = round(Number(rubAcc.s || 0), 2);
|
|
769
|
+
const posRaw = Array.isArray(ps.pos) ? ps.pos : [];
|
|
770
|
+
let posSum = 0, pnlSum = 0;
|
|
771
|
+
const positions = posRaw.map((p) => {
|
|
772
|
+
const qty = Number(p.q || 0);
|
|
773
|
+
const price = round(Number(p.mkt_price || p.price_a || 0), 4);
|
|
774
|
+
const val = round(Number(p.s || qty * price), 2);
|
|
775
|
+
const pnl = round(Number(p.vm || 0), 2);
|
|
776
|
+
posSum += val;
|
|
777
|
+
pnlSum += pnl;
|
|
778
|
+
return { symbol: String(p.i || '').replace(/\..*$/, ''), name: p.name || p.i || '', qty, price, valueRub: val, pnlRub: pnl };
|
|
779
|
+
});
|
|
780
|
+
return { accountId: acc.accountId || 'Tradernet', marginEnabled: true, totalRub: round(cashRub + posSum, 2), cashRub, totalPnlRub: round(pnlSum, 2), positions };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function syncItiCapital(acc) {
|
|
784
|
+
if (!acc.accountId) throw new Error('Укажите портфель ITI Capital (например BP12345)');
|
|
785
|
+
const oauth = acc.sandbox ? 'https://oauthdev.iticapital.ru' : 'https://oauth.iticapital.ru';
|
|
786
|
+
const api = acc.sandbox ? 'https://apidev.iticapital.ru' : 'https://api.iticapital.ru';
|
|
787
|
+
const rTok = await fetch(`${oauth}/refresh?token=${encodeURIComponent(acc.token)}`, { method: 'POST', signal: AbortSignal.timeout(15000) });
|
|
788
|
+
if (!rTok.ok) throw new Error(`ITI Capital OAuth: HTTP ${rTok.status}`);
|
|
789
|
+
const { AccessToken } = await rTok.json();
|
|
790
|
+
if (!AccessToken) throw new Error('ITI Capital не вернул AccessToken');
|
|
791
|
+
const headers = { Authorization: `Bearer ${AccessToken}` };
|
|
792
|
+
const [rSum, rPos] = await Promise.all([
|
|
793
|
+
fetch(`${api}/md/v2/Clients/MOEX/${encodeURIComponent(acc.accountId)}/summary`, { headers, signal: AbortSignal.timeout(15000) }),
|
|
794
|
+
fetch(`${api}/md/v2/Clients/MOEX/${encodeURIComponent(acc.accountId)}/positions`, { headers, signal: AbortSignal.timeout(15000) }),
|
|
795
|
+
]);
|
|
796
|
+
if (!rSum.ok) throw new Error(`ITI Capital summary: HTTP ${rSum.status}`);
|
|
797
|
+
const sum = await rSum.json();
|
|
798
|
+
const posList = rPos.ok ? await rPos.json() : [];
|
|
799
|
+
let cashRub = round(Number(sum.buyingPower || sum.cash || 0), 2);
|
|
800
|
+
const positions = [];
|
|
801
|
+
for (const p of Array.isArray(posList) ? posList : []) {
|
|
802
|
+
if (p.isCurrency || p.symbol === 'RUB') { cashRub = round(Number(p.qtyUnits || p.qty || cashRub), 2); continue; }
|
|
803
|
+
const qty = Number(p.qtyUnits || p.qty || 0);
|
|
804
|
+
const val = Number(p.volume || 0);
|
|
805
|
+
positions.push({ symbol: p.symbol, qty, price: qty ? round(val / qty, 4) : 0, valueRub: round(val, 2), pnlRub: round(Number(p.unrealisedPl || 0), 2) });
|
|
806
|
+
}
|
|
807
|
+
return { accountId: acc.accountId, marginEnabled: true, totalRub: round(Number(sum.portfolioLiquidationValue || cashRub), 2), cashRub, positions };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function syncQuikOrManual(acc) {
|
|
811
|
+
const tokenStr = String(acc.token || '').trim();
|
|
812
|
+
if (tokenStr.startsWith('http://') || tokenStr.startsWith('https://')) {
|
|
813
|
+
const base = tokenStr.replace(/\/+$/, '');
|
|
814
|
+
const r = await fetch(`${base}/portfolio?account=${encodeURIComponent(acc.accountId || '')}`, { signal: AbortSignal.timeout(12000) });
|
|
815
|
+
if (!r.ok) throw new Error(`QUIK HTTP-шлюз: HTTP ${r.status}`);
|
|
816
|
+
const j = await r.json();
|
|
817
|
+
const positions = (j.positions || []).map((p) => ({
|
|
818
|
+
symbol: p.symbol || p.secCode,
|
|
819
|
+
name: p.name || p.symbol || p.secCode,
|
|
820
|
+
qty: Number(p.qty || p.currentBal || 0),
|
|
821
|
+
price: round(Number(p.price || p.lastPrice || 0), 4),
|
|
822
|
+
valueRub: round(Number(p.valueRub || 0), 2),
|
|
823
|
+
pnlRub: round(Number(p.pnlRub || 0), 2),
|
|
824
|
+
}));
|
|
825
|
+
const cashRub = round(Number(j.cashRub ?? j.cash ?? acc.maxCapitalRub ?? 100000), 2);
|
|
826
|
+
const totalRub = round(Number(j.totalRub ?? (cashRub + positions.reduce((s, x) => s + x.valueRub, 0))), 2);
|
|
827
|
+
return { accountId: acc.accountId || 'QUIK', marginEnabled: Boolean(j.marginEnabled), totalRub, cashRub, positions };
|
|
828
|
+
}
|
|
829
|
+
// Режим подтверждения заявок (СберИнвестиции / ВТБ / Альфа без QUIK-шлюза): учёт позиций и лимита внутри платформы
|
|
830
|
+
const specs = await ensureBoardSpecs();
|
|
831
|
+
const prevPos = acc.lastSync?.positions || [];
|
|
832
|
+
let posVal = 0, pnlSum = 0;
|
|
833
|
+
const positions = prevPos.filter((p) => p.qty !== 0).map((p) => {
|
|
834
|
+
const px = specs[p.symbol]?.lastPrice || p.price || 0;
|
|
835
|
+
const avg = p.avgPrice || p.price || px;
|
|
836
|
+
const valueRub = round(p.qty * px, 2);
|
|
837
|
+
const pnlRub = round(p.qty * (px - avg), 2);
|
|
838
|
+
posVal += Math.abs(valueRub);
|
|
839
|
+
pnlSum += pnlRub;
|
|
840
|
+
return { ...p, avgPrice: avg, price: round(px, 4), valueRub, pnlRub };
|
|
841
|
+
});
|
|
842
|
+
const baseCap = acc.maxCapitalRub || 100000;
|
|
843
|
+
const cashRub = acc.lastSync?.cashRub !== undefined ? round(acc.lastSync.cashRub, 2) : round(Math.max(0, baseCap - posVal), 2);
|
|
844
|
+
const totalRub = round(cashRub + posVal, 2);
|
|
845
|
+
return { accountId: acc.accountId || 'Ручное подтверждение', marginEnabled: false, totalRub, cashRub, totalPnlRub: round(pnlSum, 2), positions };
|
|
846
|
+
}
|
|
847
|
+
|
|
670
848
|
export async function syncBrokerAccount(id) {
|
|
671
849
|
const list = loadRawAccounts();
|
|
672
850
|
const acc = list.find((x) => x.id === id);
|
|
673
851
|
if (!acc) throw new Error('Счёт не найден');
|
|
674
|
-
|
|
852
|
+
const isManualCapable = ['sber', 'vtb', 'alfa'].includes(acc.broker);
|
|
853
|
+
if (!acc.token && !isManualCapable) throw new Error('У счёта не задан токен API');
|
|
675
854
|
const today = new Date(Date.now() + MSK).toISOString().slice(0, 10);
|
|
676
855
|
try {
|
|
677
856
|
let data;
|
|
@@ -679,6 +858,9 @@ export async function syncBrokerAccount(id) {
|
|
|
679
858
|
else if (acc.broker === 'finam') data = await syncFinam(acc);
|
|
680
859
|
else if (acc.broker === 'alor') data = await syncAlor(acc);
|
|
681
860
|
else if (acc.broker === 'bcs') data = await syncBcs(acc);
|
|
861
|
+
else if (acc.broker === 'cifra') data = await syncCifra(acc);
|
|
862
|
+
else if (acc.broker === 'iticapital') data = await syncItiCapital(acc);
|
|
863
|
+
else if (isManualCapable) data = await syncQuikOrManual(acc);
|
|
682
864
|
else throw new Error(`Неизвестный брокер: ${acc.broker}`);
|
|
683
865
|
if (data.accountId && !acc.accountId) acc.accountId = data.accountId;
|
|
684
866
|
if (data.accountName) acc.tbankAccountName = data.accountName;
|
|
@@ -765,9 +947,10 @@ export async function prepareBrokerOrders(id) {
|
|
|
765
947
|
}
|
|
766
948
|
const baseRun = acc.baseRunId ? getRun(Number(acc.baseRunId)) : null;
|
|
767
949
|
const params = normalizeParams(baseRun?.params || getSettings());
|
|
768
|
-
const [fc, specs] = await Promise.all([
|
|
950
|
+
const [fc, specs, shortableMap] = await Promise.all([
|
|
769
951
|
forecastNow(params),
|
|
770
952
|
ensureBoardSpecs(),
|
|
953
|
+
ensureTbankShortableMap(acc),
|
|
771
954
|
]);
|
|
772
955
|
const capCeiling = plan.maxCapitalPerAccountRub > 0 ? plan.maxCapitalPerAccountRub : 1000000000;
|
|
773
956
|
const effectiveMaxCap = Math.min(acc.maxCapitalRub, capCeiling);
|
|
@@ -817,7 +1000,17 @@ export async function prepareBrokerOrders(id) {
|
|
|
817
1000
|
const nb = params.newsFilter ? blockingNews(6) : null;
|
|
818
1001
|
|
|
819
1002
|
const buyCands = fc.rows.filter((r) => r.signal && !quarantined.has(r.symbol) && !nb?.get(r.symbol)?.bad).sort((a, b) => b.expectedPct - a.expectedPct).slice(0, K);
|
|
820
|
-
const
|
|
1003
|
+
const allShortSignals = rawDir !== 'long' ? fc.rows.filter((r) => r.shortSignal && !quarantined.has(r.symbol) && !nb?.get(r.symbol)?.good).sort((a, b) => a.expectedPct - b.expectedPct) : [];
|
|
1004
|
+
const nonShortableSkipped = [];
|
|
1005
|
+
const rawShortCands = [];
|
|
1006
|
+
for (const r of allShortSignals) {
|
|
1007
|
+
if (shortableMap && shortableMap[r.symbol] === false) {
|
|
1008
|
+
nonShortableSkipped.push(r);
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
rawShortCands.push(r);
|
|
1012
|
+
if (rawShortCands.length >= K) break;
|
|
1013
|
+
}
|
|
821
1014
|
const shortCands = (marginAllowed && planAllowsShorts) ? rawShortCands : [];
|
|
822
1015
|
|
|
823
1016
|
const legacyMode = acc.legacyMode || 'isolate';
|
|
@@ -976,7 +1169,8 @@ export async function prepareBrokerOrders(id) {
|
|
|
976
1169
|
const skippedShorts = rawShortCands.map((r) => `${r.symbol} (${r.expectedPct}%)`).join(', ');
|
|
977
1170
|
diagNote = `Счёт автоматически работает в режиме «Только в рост (без шорта)», так как на нём в Т-Инвестициях отключена маржинальная торговля. Сигналы на падение (${skippedShorts}) пропущены; новых сигналов на покупку на свече ${fc.bar} нет. Если хотите зарабатывать и на падении (шортах), включите «Маржинальную торговлю» в настройках этого счёта в приложении Т-Инвестиций.`;
|
|
978
1171
|
} else if (!newBuys.length && !newShorts.length) {
|
|
979
|
-
|
|
1172
|
+
const noShortNote = nonShortableSkipped.length ? ` (пропущены немаржинальные бумаги, недоступные для шорта у брокера: ${nonShortableSkipped.map((r) => `${r.symbol} ${r.expectedPct}%`).join(', ')})` : '';
|
|
1173
|
+
diagNote = `На свече ${fc.bar} ни одна доступная бумага не превысила порог выгоды +${fc.thresholdPct}%${noShortNote}.`;
|
|
980
1174
|
}
|
|
981
1175
|
}
|
|
982
1176
|
|
|
@@ -1059,7 +1253,7 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
1059
1253
|
if (Number.isFinite(maxLots)) {
|
|
1060
1254
|
if (maxLots < 1) {
|
|
1061
1255
|
if (ord.action === 'short') {
|
|
1062
|
-
throw new Error(
|
|
1256
|
+
throw new Error(`Короткая продажа (шорт) по бумаге ${ord.symbol} недоступна у брокера (бумага не входит в список маржинальных для шорта или исчерпан лимит займа)`);
|
|
1063
1257
|
}
|
|
1064
1258
|
throw new Error(`У брокера недостаточно доступного лимита для ${isBuy ? 'покупки' : 'продажи'} ${ord.symbol} (доступно 0 лотов)`);
|
|
1065
1259
|
}
|
|
@@ -1153,6 +1347,97 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
1153
1347
|
const j = await r.json();
|
|
1154
1348
|
return { ok: true, orderId: j.orderId || body.clientOrderId };
|
|
1155
1349
|
}
|
|
1350
|
+
if (acc.broker === 'cifra') {
|
|
1351
|
+
const [apiKey, secKey] = String(acc.token || '').split(':').map((s) => s.trim());
|
|
1352
|
+
if (!apiKey || !secKey) throw new Error('Для Цифра брокер укажите ключи в формате PublicKey:SecretKey');
|
|
1353
|
+
const qObj = {
|
|
1354
|
+
cmd: 'putTradeOrder',
|
|
1355
|
+
params: {
|
|
1356
|
+
instr_name: `${ord.symbol}.MOEX`,
|
|
1357
|
+
action_id: ord.action === 'buy' || ord.action === 'cover' ? 1 : 3,
|
|
1358
|
+
order_type_id: 2,
|
|
1359
|
+
curr: 'RUR',
|
|
1360
|
+
limit_price: ord.limitPrice,
|
|
1361
|
+
qty: ord.shares || ord.lots,
|
|
1362
|
+
},
|
|
1363
|
+
};
|
|
1364
|
+
const qStr = `q=${JSON.stringify(qObj)}`;
|
|
1365
|
+
const sig = crypto.createHmac('sha256', secKey).update(qStr).digest('hex');
|
|
1366
|
+
const r = await fetch('https://tradernet.ru/api/v2/cmd/putTradeOrder', {
|
|
1367
|
+
method: 'POST',
|
|
1368
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-NtApi-PublicKey': apiKey, 'X-NtApi-Sig': sig },
|
|
1369
|
+
body: qStr,
|
|
1370
|
+
signal: AbortSignal.timeout(15000),
|
|
1371
|
+
});
|
|
1372
|
+
if (!r.ok) throw new Error(`Tradernet PostOrder HTTP ${r.status}`);
|
|
1373
|
+
const j = await r.json();
|
|
1374
|
+
return { ok: true, orderId: String(j.order_id || j.result?.order_id || 'ok') };
|
|
1375
|
+
}
|
|
1376
|
+
if (acc.broker === 'iticapital') {
|
|
1377
|
+
const oauth = acc.sandbox ? 'https://oauthdev.iticapital.ru' : 'https://oauth.iticapital.ru';
|
|
1378
|
+
const api = acc.sandbox ? 'https://apidev.iticapital.ru' : 'https://api.iticapital.ru';
|
|
1379
|
+
const rTok = await fetch(`${oauth}/refresh?token=${encodeURIComponent(acc.token)}`, { method: 'POST', signal: AbortSignal.timeout(15000) });
|
|
1380
|
+
if (!rTok.ok) throw new Error(`ITI Capital OAuth HTTP ${rTok.status}`);
|
|
1381
|
+
const { AccessToken } = await rTok.json();
|
|
1382
|
+
const body = {
|
|
1383
|
+
side: ord.action === 'buy' || ord.action === 'cover' ? 'buy' : 'sell',
|
|
1384
|
+
type: 'limit',
|
|
1385
|
+
quantity: ord.lots,
|
|
1386
|
+
price: ord.limitPrice,
|
|
1387
|
+
instrument: { symbol: ord.symbol, exchange: 'MOEX', instrumentGroup: 'TQBR' },
|
|
1388
|
+
user: { portfolio: acc.accountId },
|
|
1389
|
+
};
|
|
1390
|
+
const r = await fetch(`${api}/commandapi/warptrans/TRADE/v2/client/orders/actions/limit`, {
|
|
1391
|
+
method: 'POST',
|
|
1392
|
+
headers: { Authorization: `Bearer ${AccessToken}`, 'Content-Type': 'application/json', 'X-ALOR-REQID': crypto.randomUUID() },
|
|
1393
|
+
body: JSON.stringify(body),
|
|
1394
|
+
signal: AbortSignal.timeout(15000),
|
|
1395
|
+
});
|
|
1396
|
+
if (!r.ok) throw new Error(`ITI Capital PostOrder HTTP ${r.status}`);
|
|
1397
|
+
const j = await r.json();
|
|
1398
|
+
return { ok: true, orderId: j.orderNumber || 'ok' };
|
|
1399
|
+
}
|
|
1400
|
+
if (['sber', 'vtb', 'alfa'].includes(acc.broker)) {
|
|
1401
|
+
const tokenStr = String(acc.token || '').trim();
|
|
1402
|
+
if (tokenStr.startsWith('http://') || tokenStr.startsWith('https://')) {
|
|
1403
|
+
const base = tokenStr.replace(/\/+$/, '');
|
|
1404
|
+
const r = await fetch(`${base}/orders`, {
|
|
1405
|
+
method: 'POST',
|
|
1406
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1407
|
+
body: JSON.stringify({ accountId: acc.accountId, ...ord }),
|
|
1408
|
+
signal: AbortSignal.timeout(12000),
|
|
1409
|
+
});
|
|
1410
|
+
if (!r.ok) throw new Error(`QUIK HTTP-шлюз PostOrder: HTTP ${r.status}`);
|
|
1411
|
+
const j = await r.json();
|
|
1412
|
+
return { ok: true, orderId: j.orderId || 'quik_ok' };
|
|
1413
|
+
}
|
|
1414
|
+
// Режим ручного подтверждения (СберИнвестиции / ВТБ / Альфа без QUIK): учитываем исполнение в портфеле платформы
|
|
1415
|
+
const prevPos = [...(acc.lastSync?.positions || [])];
|
|
1416
|
+
const isBuy = ord.action === 'buy' || ord.action === 'cover';
|
|
1417
|
+
const deltaQty = isBuy ? (ord.shares || ord.lots) : -(ord.shares || ord.lots);
|
|
1418
|
+
const deltaCash = isBuy ? -ord.amountRub : ord.amountRub;
|
|
1419
|
+
const pIdx = prevPos.findIndex((p) => p.symbol === ord.symbol);
|
|
1420
|
+
if (pIdx >= 0) {
|
|
1421
|
+
const cur = prevPos[pIdx];
|
|
1422
|
+
const nextQty = cur.qty + deltaQty;
|
|
1423
|
+
if (nextQty === 0) prevPos.splice(pIdx, 1);
|
|
1424
|
+
else prevPos[pIdx] = { ...cur, qty: nextQty, price: ord.limitPrice, valueRub: round(nextQty * ord.limitPrice, 2) };
|
|
1425
|
+
} else if (deltaQty !== 0) {
|
|
1426
|
+
prevPos.push({ symbol: ord.symbol, name: ord.symbol, instrumentType: 'share', qty: deltaQty, lots: ord.lots, avgPrice: ord.limitPrice, price: ord.limitPrice, valueRub: round(deltaQty * ord.limitPrice, 2), pnlRub: 0 });
|
|
1427
|
+
}
|
|
1428
|
+
const prevCash = acc.lastSync?.cashRub ?? (acc.maxCapitalRub || 100000);
|
|
1429
|
+
const nextCash = round(Math.max(0, prevCash + deltaCash), 2);
|
|
1430
|
+
const posSum = prevPos.reduce((s, p) => s + Math.abs(p.valueRub || 0), 0);
|
|
1431
|
+
acc.lastSync = {
|
|
1432
|
+
...(acc.lastSync || {}),
|
|
1433
|
+
at: Date.now(),
|
|
1434
|
+
ok: true,
|
|
1435
|
+
cashRub: nextCash,
|
|
1436
|
+
totalRub: round(nextCash + posSum, 2),
|
|
1437
|
+
positions: prevPos,
|
|
1438
|
+
};
|
|
1439
|
+
return { ok: true, orderId: 'учтено в портфеле' };
|
|
1440
|
+
}
|
|
1156
1441
|
throw new Error(`Неизвестный брокер: ${acc.broker}`);
|
|
1157
1442
|
}
|
|
1158
1443
|
|