- funding_rate table and "Фондирование" page: rate from date, history, rate on date (D27)
- positions by issue: BUY - SELL per portfolio, "Остатки" page (D29)
- quotes are loaded only for securities with an open position (D10)
- business calendar from xmlcalendar.ru: yearly file upload, report date
and update job use previous working day (D30)
- xlsx download for positions, quotes and funding rate history
- manual operation type as radio buttons
- QUIK instruments with purchase marks ("МТС 2Р-18_1") match the issue,
file name kept in operation.instrument (D17)
- migrations d3a6f8b1c254, e7b2c9d4a813, f1c4a7e9b352
- project.md: decisions and questions after customer meetings
122 lines
23 KiB
Markdown
122 lines
23 KiB
Markdown
# Портфели ДРРК (BIQ-38115)
|
||
|
||
Решения (D…) и открытые вопросы (Q…), на которые ссылается код. Где бизнес-решения нет, в коде `TODO(BIQ-38115)` с номером вопроса.
|
||
|
||
## Что это
|
||
|
||
Сервис заменяет два Excel-файла с VBA-макросами, в которых ДРРК ведёт портфели облигаций:
|
||
- UW — портфель андеррайтинга («Отчет по портфелю ДСМ_2026.xlsm»);
|
||
- SALES — портфель продаж («Отчет по портфелю ПП_2026.xlsm»).
|
||
|
||
Плюс сводный отчёт по обоим портфелям (сейчас собирается руками).
|
||
|
||
Текущие макросы и формулы: `docs/legacy_excel.md`, исходники VBA: `docs/vba/`.
|
||
|
||
## Стек и платформа
|
||
|
||
- Раиса (k8s), Streamlit, PostgreSQL. Авторизация — Keycloak через `raisa-streamlit-oauth`.
|
||
- async SQLAlchemy + asyncpg, Alembic со схемой.
|
||
- БД из Streamlit — через `run_db()` (движок с `NullPool`).
|
||
- Внешние данные — через шлюз Gravitee.
|
||
- Интеграции с QUIK нет: пользователь выгружает отчёт по сделкам в .xlsx и загружает файл в сервис целиком (D19).
|
||
|
||
## Внешние данные
|
||
|
||
Проверено на тестовом контуре 09.10.2026.
|
||
|
||
- Gravitee: тест `https://gateway.rshbdev.ru`, прод `https://gateway.rshb.pro`.
|
||
- Заголовок `X-Rshb-Api-Key`. У MOEX и ЦБ разные ключи (ключ ЦБ на MOEX — 401).
|
||
- MOEX: `/external/moex-iss` → `https://iss.moex.com`, в запросах нужен `/iss/...`. `/external/moex` не существует (404 «No context-path matches the request URI»).
|
||
- ЦБ: `/external/cbr` → `https://cbr.ru`.
|
||
- История и параметры выпуска на ISS — по SECID, не по ISIN (у ОФЗ они разные). Добавление бумаги: `/iss/securities.json?q={ISIN}` → SECID → `/iss/securities/{SECID}.json` (D25).
|
||
- В истории MOEX есть больше, чем берёт макрос: `FACEVALUE` (номинал после амортизации), `COUPONPERCENT`, `BONDTYPE`, признак оферты, `NUMTRADES`, `VOLUME`. Храним.
|
||
- `DURATION` — в днях, в годы /365 (D23).
|
||
- Пример: РЭО-02 (RU000A106169) за 08.10.2026 — TQCB, WAPRICE 101.42, MARKETPRICE3 101.38, YIELDATWAP 17.78, YIELDCLOSE 17.98, DURATION 759, ACCINT 3.68, FACEVALUE 645.19, COUPONPERCENT 17.35, NUMTRADES 3, VOLUME 15.
|
||
- `X-Gravitee-Transaction-Id` каждого запроса сохраняем (D18).
|
||
|
||
## Объёмы
|
||
|
||
- Сделки: UW — 4150 за 2026 (2848 за 2025), SALES — ~860 в год. До 1660 сделок по одному выпуску, в день размещения в выгрузке 1000+ строк — поэтому сделки только файлом, одной транзакцией. Вручную — только то, чего нет в QUIK (D21).
|
||
- РЕПО: ~150 сделок в год, лист РЕПО не переносим.
|
||
- Котировки: 1–10 открытых бумаг в день, ~1,5 КБ на бумагу. Курс ЦБ: 1 запрос в день.
|
||
|
||
## Выгрузка QUIK
|
||
|
||
Пример: `tests/fixtures/quik_sample_300326_anon.xlsx` (обезличенный, оригинал вне git, см. README). Формат «Лист2», 21 колонка:
|
||
Дата сделки, Время, Дата расчетов, Инструмент сокр., Операция (Купля/Продажа), Счет, Кол-во, Цена, Объем, Расчетная сумма, Комментарий, Код расчетов, Заявка, Орг-я партнера, Инструмент сокр., Валюта расчетов, Комиссия ТС, Клиринговая комиссия, ФБ комиссия, ТЦ комиссия, Комиссия брокера.
|
||
|
||
- «Лист1» — другой формат: без комментария, заявки и комиссий, но с НКД и доходностью. Не поддерживаем. Лист ищем по заголовкам, не по имени.
|
||
- Колонка A — номер строки без заголовка. «Инструмент сокр.» дважды (E и P). После данных бывают пустые строки.
|
||
- Даты и время — значения Excel с форматом даты (`datetime`/`time` в openpyxl). Принимаем также серийные числа / долю суток и текст `ДД.ММ.ГГГГ` / `ЧЧ:ММ:СС`.
|
||
- `SUR` = RUB. Номер заявки — 11 цифр, храним строкой.
|
||
- НКД в «Лист2» нет. В примере (размещение) «Расчетная сумма» = «Объем»; какая из них «сумма с НКД» — Q17.
|
||
- «Объем» = кол-во × цена × 10 (номинал 1000).
|
||
- Выгрузка — раз в день вечером, после всех сделок дня.
|
||
- Счёт у обоих портфелей один. Комментарий: `/uw` — UW, `/pp` — SALES, `/trde` — торговый портфель, к ДРРК не относится. Портфель выбирает пользователь при загрузке, комментарий — подсказка (D19).
|
||
- Номера сделки нет, только номер заявки (одна заявка — несколько сделок). Дедупликации по строкам нет, защита от повтора — на уровне файла (D19, Q2).
|
||
- Купоны, амортизация, комиссия за андеррайтинг и корректировки в QUIK не приходят — вводятся вручную (D21, D22).
|
||
|
||
## Решения
|
||
|
||
| # | Решение |
|
||
|---|---|
|
||
| D1 | PK — BIGINT identity везде |
|
||
| D2 | Общий справочник бумаг, ISIN уникален (ISIN = выпуск). Принадлежность к UW/SALES — через операции, одна бумага может быть в обоих портфелях. Лотов нет (D20) |
|
||
| D3 | Параметры бумаги и операции хранятся раздельно; объём и цена покупки — в операции BUY |
|
||
| D4 | «Дата размещения» — дата покупки/размещения у банка, вводит пользователь (по умолчанию сегодня), не дата выпуска на бирже. Дата начала торгов с MOEX — отдельно, справочно. MOEX не запрашиваем, пока дата отчёта ≤ даты размещения |
|
||
| D5 | «Продать до» — дата на пару «портфель + бумага» (D28), вводится вручную: UW — срок из решения комитета, SALES — не более 90 дней от даты покупки (стратегия портфеля). К этой дате позиции в портфеле уже нет: при расчётах T+1 продажа накануне. Формулы Excel (+364, +28) не переносятся. Разный срок у покупок одного выпуска — Q24 |
|
||
| D6 | Ручная цена на дату в приоритете и не затирается загрузкой MOEX; значение MOEX хранится рядом; кто/когда/причина. Править можно цену, доходность, дюрацию. Отдельная таблица `market_data_manual`, пустое поле — действует MOEX; повторная правка на дату перезаписывает |
|
||
| D7 | Комиссии — пять отдельных полей. «Комиссия ТС» = клиринговая + ФБ (8217,60 = 3492,48 + 4725,12; в файле продаж 2026 — во всех 285 сделках с комиссией). «Комиссии МБ» в отчёте = сумма «Комиссии ТС», как в Excel. Все пять не суммируются — двойной счёт |
|
||
| D8 | Режимы торгов: TQCB, затем TQOB. Если есть только другие борды (например PACT) — ошибка, а не «торгов не было». Цена при отсутствии торгов — D33 |
|
||
| D9 | Курсы CNY грузим с даты самой ранней CNY-операции |
|
||
| D10 | Котировки обновляем только по бумагам с открытой позицией на дату отчёта: остаток (D29) хоть в одном портфеле не 0, отрицательный тоже |
|
||
| D11 | История номинала (`security_nominal_history`) с источником `moex` / `manual`. Заполняется из `FACEVALUE` при загрузке котировок, только когда он отличается от последнего значения MOEX; ручное значение на дату в приоритете и действует до следующего изменения на MOEX. До первой записи номинала нет; прошлые значения и номинал после амортизации, если MOEX не дал, вводятся вручную |
|
||
| D12 | Исходные данные храним всегда; текущий день считаем на лету; при закрытии дня — снапшот итогов для T-1, истории и сводного отчёта (Q20) |
|
||
| D13 | Табличный редактор и редактируемые формулы вне скоупа: параметры в справочниках + экспорт в .xlsx. Формулы свода простые, пользователь их не правит |
|
||
| D14 | Лист `control` и лист РЕПО не переносим. Производственный календарь нужен (D30) |
|
||
| D15 | Деньги и цены — `Decimal`/`Numeric`, не float |
|
||
| D16 | Внешние API — только по кнопке и в джобе, экраны читают из БД |
|
||
| D17 | Сделка QUIK сопоставляется с бумагой по «Инструмент сокр.» = SHORTNAME MOEX (`security.name`). Имя с меткой покупки после последнего «_» («МТС 2Р-18_1», «РЖД 1Р-49R_Д») — тот же выпуск, отдельные покупки (D20); точное совпадение имени важнее. Имя из файла хранится в `operation.instrument` |
|
||
| D18 | В `external_request_log` пишем `X-Gravitee-Transaction-Id` ответа шлюза. Остальные заголовки и ключи не сохраняем и не логируем |
|
||
| D19 | Повтор загрузки QUIK отсекаем на уровне файла: уникальность (хэш содержимого, портфель). Хэш — по нормализованным строкам, пересохранённый в Excel файл с теми же данными тоже повтор. Один файл — в каждый портфель по разу, каждая загрузка берёт строки своего портфеля (`/uw` → UW, `/pp` → SALES). Строки с пустым, неизвестным комментарием и `/trde` — отдельным блоком: отнести к выбранному портфелю или исключить. Строка, уже загруженная из этого файла в другой портфель, повторно не грузится. В `import_batch` — сколько строк загружено и исключено; на странице — «загружено в UW / в SALES / никуда». Пересечение периода с другим файлом того же портфеля — предупреждение |
|
||
| D20 | Лотов нет: ISIN = выпуск, каждая покупка — отдельная операция BUY. Учёт партий — Q24 |
|
||
| D21 | Вручную вводятся только купоны (CPN), амортизация (NOM), комиссия за андеррайтинг (D22) и корректировки (ADJ). Купля/продажа — только файлом |
|
||
| D22 | Комиссия за андеррайтинг — операция `UWFEE` (дата + сумма) по выпуску, только в UW; их может быть несколько, в своде суммируются. «ЧОД с учётом комиссии андеррайтинга» = P/L (Net) + комиссия |
|
||
| D23 | Дюрация = `DURATION` MOEX / 365, как в макросе |
|
||
| D24 | Сводный отчёт: портфель MM не делаем; группировка по типу купона (фикс/флоутер); MD и DV01 (сверено на РЭО-02): MD = дюрация_лет / (1 + YTW_текущая / 100) (0,1 / 1,1813 = 0,08465); объём по номиналу = остаток × номинал на дату × FX; DV01 (1 б.п.) = MD × объём × 0,0001 (0,018 млн ₽); «DV01×100» = DV01 × 100 (1,7996); % лимита DV01 = DV01 / 10 млн (0,18 %). Единицы — Q27, AFS и лимиты — Q4 |
|
||
| D25 | SECID ищем по ISIN один раз при добавлении бумаги и храним в `security`; дальше все запросы к ISS по SECID |
|
||
| D26 | Отмены загрузки файла, удаления ручных операций и ставок фондирования, сброса ручных значений (цена, номинал) пока нет |
|
||
| D27 | Ставки фондирования — `funding_rate`: портфель, валюта, дата начала действия, ставка % годовых (как таблица ставок FUNDING в Excel). Ставка действует до следующей записи, до первой записи ставки нет; повторный ввод на дату перезаписывает. Ставки устанавливает комитет, ставка общая для валюты; индивидуальные — Q8. База фондирования — номинал, как в Excel (покупки почти всегда по номиналу). День продажи — Q19 |
|
||
| D28 | Принадлежность бумаги к портфелю — по операциям (D2), операции на пару «портфель + бумага» не ссылаются. Параметры пары (плановая дата продажи, дата перехода в пост-андеррайтинг) — отдельная таблица `portfolio_security`, без истории статусов (D5, D31). Ключ пары при учёте партий — Q24 |
|
||
| D29 | Остаток выпуска в портфеле, шт = Σ BUY − Σ SELL по портфелю и бумаге. Амортизация (NOM) меняет номинал, не количество. Погашение — Q29, дата отбора операций — Q30 |
|
||
| D30 | Производственный календарь — файл на год с xmlcalendar.ru (`data/ru/ГГГГ/calendar.json`), загружается на странице «Календарь» в `non_working_day`; повторная загрузка заменяет год. Рабочие субботы учитываются. Дата отчёта по умолчанию и в джобе — предыдущий рабочий день; год без календаря — только выходные |
|
||
| D31 | Пост-андеррайтинг — только из UW, весь остаток выпуска, без обратного перехода. Цена покупки, количество и расчёт не меняются, меняется раздел в своде («переведены в торговый субпортфель пост-андеррайтинг»). Достаточно даты перехода в `portfolio_security` |
|
||
| D32 | Переход года: открытая позиция закрывает год по средневзвешенной цене последнего торгового дня года и входит в новый год покупкой 01.01 по этой же цене, сумма с НКД (в ДСМ_2026 — РЭО-02 по 97,97). Фондирование продолжается, ставка с нового года — по `funding_rate` (D27). Как это устроено в сервисе — Q33 |
|
||
| D33 | Нет торгов на дату — цена (средневзвешенная) предыдущего торгового дня. Ручная правка цены (D6) возможна, на практике не используется |
|
||
| D34 | В своде вручную, как в Excel: дата оферты или погашения, тип купона (фикс/флоатер), тип бумаги (выбор из списка, Q4), «Продать до» (D5), комиссия за андеррайтинг (D22). Режим торгов сделки для расчёта не важен, в INPUTS — только код расчётов |
|
||
|
||
## Открытые вопросы
|
||
|
||
- Q2. Есть ли номер сделки в свежей выгрузке QUIK. Ключ «заявка + время + кол-во + цена» неуникален: в файле продаж 2026 — 5 пар разных сделок с одинаковым ключом.
|
||
- Q4. Сводный отчёт: «AFS Портфель из ДУ» (возможно, убирается); классификатор типа бумаги (КОРП/Муниц/Иностр/ОФЗ/Хедж/Торг/ПАИ/Репо); лимиты (DV01, КОРП 25 000 млн по номиналу, сигнальные, стоп-лосс, 480-П) и контроль превышений.
|
||
- Q5. Доходность покупки (YTW) — свойство операции покупки или бумаги. В макросе — поле бумаги (SECURITIES INFO, кол. O).
|
||
- Q6. Участвует ли NOM в реализованном результате (в Excel — только для РЭО-02, в первой строке CALC).
|
||
- Q7. Операция VAL: смысл неизвестен, в данных нет, в формуле фондирования наравне с Sell.
|
||
- Q8. Ставка РЭО-02 в Excel = ключевая + 2,42, при том что ставка общая для валюты (D27): исключение или ошибка. Если индивидуальные ставки появятся, они вводятся вручную на бумагу.
|
||
- Q9. Реализованная переоценка в Excel считает номинал 1000 (×10), нереализованная — по фактическому. Ошибка или так задумано.
|
||
- Q10. Неразмещённая бумага: в макросе цена 100, НКД 0, доходность покупки.
|
||
- Q13. RES_HISTORY: переносить ли и как (колонки в Excel сдвинуты, см. legacy_excel.md).
|
||
- Q14. С какого режима брать цену (D8): бумага торгуется в нескольких режимах (размещение, T+, РПС с ЦК, ОТС с ЦК, РПС), продажи одного выпуска идут во всех.
|
||
- Q16. Права на ручные значения (цена, номинал, ручные операции).
|
||
- Q17. Сумма с НКД: «Расчетная сумма» из QUIK? Для CPN/NOM — вводится или считается.
|
||
- Q19. Фондирование: «дата сделки − 1» в Excel — исключение дня продажи из начисления или подгонка под результат; в ПП_2026 в CALC!L5:L57 (январские продажи) без − 1, разница 95 735 р.
|
||
- Q20. Хранение результатов расчёта (D12).
|
||
- Q21. Параметры при добавлении по ISIN: наименование, номинал, купон, дата начала торгов — с MOEX; дата размещения, объём, цена, YTW — вводит пользователь (D3, D4).
|
||
- Q24. Партии внутри выпуска. Покупки по разной цене пользователь разделяет суффиксом в «Инструмент сокр.» («МТС 2Р-18_1»), правя файл QUIK до загрузки, и тем же суффиксом помечает продажу, которая закрывает эту покупку; внутри одного имени Excel усредняет. Метка хранится в операции (D17). Что значит метка «_Д» («РЖД 1Р-49R_Д»). Как хранить партию и какая цена покупки при частичной продаже (формулы CALC).
|
||
- Q27. Единицы MD и DV01 в сводном отчёте (млн ₽, «DV01×100», % лимита).
|
||
- Q28. Что корректирует ADJ (количество, сумму, результат) и как участвует в расчёте. Пока хранится и в расчёт не входит.
|
||
- Q29. Окончательное погашение: на практике не было; ожидается CPN + NOM на весь номинал, штуки при этом остаются (D29), номинал 0. Считать ли такую позицию закрытой (котировки D10, свод).
|
||
- Q30. Дата отбора операций (остатки, история, фондирование): Excel считает по дате расчётов (CALC!B = «Дата расчетов» из INPUTS), НКД — на дату расчётов; у нас пока по дате сделки.
|
||
- Q32. Ручной ввод купли/продажи в исключительных случаях (сейчас только файлом, D21).
|
||
- Q33. Переход года в сервисе (D32): операции закрытия и открытия на границе года или результат за год от цены на конец прошлого года; RES_HISTORY — Q13.
|