DRRK/docs/project.md
Raykov-MS c1c73e96f5 Add funding rates, positions, business calendar and xlsx export
- 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
2026-10-29 11:20:15 +03:00

122 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Портфели ДРРК (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.