diff --git a/AI_INDEX.md b/AI_INDEX.md index 3e63df3..98eaab7 100644 --- a/AI_INDEX.md +++ b/AI_INDEX.md @@ -52,6 +52,8 @@ Cross-area topology — [system overview](architecture/system-overview.md). | Счета и их сводки | [Brokerage accounts](features/brokerage-accounts.md) | | Общий портфель и разделы по всем счетам | [All accounts](features/all-accounts.md) | | Страница акции, поиск, график и история одной бумаги | [Stock detail](features/stock-detail.md) | +| Страница облигации, поиск, купоны и переходы из портфеля | [Bond detail](features/bond-detail.md) | +| Источники кредитных рейтингов облигаций | [Исследование источников](research/bond-credit-ratings.md) | | Позиции/суммы портфеля, backend cache | [Portfolio](features/portfolio-view.md) | | Операции/фильтры/cursor | [Account ledger](features/brokerage-account-ledger.md) | | Будущие выплаты | [Upcoming events](features/brokerage-account-upcoming-events.md) | diff --git a/features/bond-detail.md b/features/bond-detail.md new file mode 100644 index 0000000..5820cc9 --- /dev/null +++ b/features/bond-detail.md @@ -0,0 +1,127 @@ +# Страница облигации + +Раздел «Облигации» открывает `/bonds`: поиск по названию, тикеру или ISIN +с двух символов. `/bonds/:identifier` принимает UID или точный тикер. +Название и логотип облигации в портфеле счёта и «Всех счетах» ведут на её +страницу по точному UID без предварительной загрузки карточек всех позиций. +При открытии выпуска проверенный однозначный тикер заменяет UID в адресе, +сохраняя период графика и фильтр событий. + +Поиск выбирает один режим на нормализованный ISIN, сохраняя разные выпуски +и записи без ISIN отдельно. Приоритет: доступность API, точный тикер, TQOB, +затем UID. Ограничение — 30 результатов после выбора режима. Цена и ссылка +принадлежат выбранному UID. Прямая ссылка из портфеля сохраняет исходный UID, +если поиск предпочитает другой режим. Неоднозначный точный тикер возвращает +`409 BOND_TICKER_AMBIGUOUS`; интерфейс предлагает выбрать выпуск через поиск. +Поиск загружает параметры выпусков и котировки, но оставляет ссылки по UID: +отдельная проверка тикера для каждого результата не выполняется. + +## Содержание + +Карточка показывает цену в процентах от номинала, время котировки и статус +торгов. Изменение выражено в процентных пунктах относительно последней +завершённой дневной свечи до московской даты котировки; база ищется за 20 дней. +Денежная оценка без НКД равна цене в процентах, умноженной на текущий номинал +и делённой на 100. Она округляется до девяти десятичных знаков. Оценка с НКД +добавляет текущий накопленный купонный доход только при совпадении валют. +Это ориентир без комиссии: котировка может быть старше текущего номинала и НКД. +FX-конвертация не выполняется, первоначальный номинал не заменяет текущий. + +Условия выпуска: текущий и первоначальный номинал, погашение, оферта, +количество купонов в год, лот. Отдельно отмечены плавающий купон, +амортизация, бессрочность и субординация. Неизвестные даты и суммы показываются +как недоступные. + +Сводка «Купон, рейтинг и доходность» показывает ближайший купон на одну +облигацию, дату выплаты и тип из календаря Т-Банка. Неизвестная сумма ближайшей +выплаты не заменяется суммой более позднего купона. Флаг плавающего купона +используется только как fallback типа; отсутствие флага не означает +фиксированный купон. + +Текущая купонная доходность — ближайший купон × 365 / длительность его периода +в днях / денежная цена без НКД × 100 %. Decimal-расчёт округляется до четырёх +знаков. Требуются известная неотрицательная выплата, положительные цена и +период и одинаковая валюта выплаты и цены. Это годовая оценка при сохранении +размера купона и номинала, без налогов, комиссий и реинвестирования; она не +является доходностью к погашению. Время котировки показывается отдельно. + +Эффективная доходность берётся из `marketdata_yields.EFFECTIVEYIELD` MOEX ISS, +с проверкой точных SECID, ISIN и BOARDID. Показываются собственная цена +расчёта, дата торгов и целевая дата (погашение, оферта или другая дата источника). +Котировка MOEX может отличаться от котировки Т-Банка и поступать с задержкой. +Доходность не рассчитывается из неполного календаря будущих выплат. + +Кредитный рейтинг выпуска читается по точному ISIN из публичного +[реестра НКР](https://ratings.ru/ratings/issues/). Отображаются агентство, +дата рейтингового действия, время получения реестра и ссылка на выпуск. +Отзыв показывается явно; уровень риска Т-Банка и рейтинг эмитента не +подставляются вместо рейтинга выпуска. Отсутствие записи в НКР не означает +отсутствия рейтингов других агентств. Покрытие и альтернативы исследованы +в [обзоре источников](../research/bond-credit-ratings.md). + +`NcrRatingsClient` разбирает HTML через Cheerio: штатного открытого API для +этой таблицы не подтверждено. Общий публичный реестр кэшируется на шесть часов, +незавершённые загрузки объединяются; повторные запросы после ошибки ограничены +одной минутой. Изменение структуры таблицы, неоднозначные данные и ошибки +загрузки означают недоступность источника, а не отсутствие рейтинга. + +График и таблица дневных цен сохраняют проценты от номинала и признак +завершённости свечи. Исторические котировки не пересчитываются в деньги +по сегодняшнему номиналу. Денежная средняя покупки не наносится на процентную +шкалу. Периоды: месяц, полгода, год и вся известная история. Окна upstream +не превышают 365 дней, одинаковые границы дедуплицируются. Если начало истории +неизвестно, запрос «вся история» возвращает `BOND_HISTORY_START_UNAVAILABLE`. + +Позиция агрегирует только `BOND` с точным UID по доступным счетам, используя +существующий Portfolio Snapshot и decimal-арифметику. Средняя покупка взвешена +по количеству. Неполные или разновалютные суммы недоступны; для сочетания +длинных и коротких позиций общая средняя и процент не синтезируются. +Результат открытой позиции не включает выплаченные купоны и продажи и не +называется доходностью к погашению. При отсутствии позиции блок скрыт. + +## Купоны и личные события + +Календарь охватывает пять прошлых лет и следующий год. Будущие купоны +сортируются по дате выплаты, прошедшие и недатированные доступны отдельно. +Показаны сумма на облигацию, номер, тип, длительность периода и дата реестра, +если они известны. Неопределённый будущий купон не заменяется нулём. + +Личная оценка доступна только при положительных позициях, известной сумме, +будущей выплате и известном ещё не прошедшем реестре. Она не подтверждает право +на выплату и не учитывает налоги и будущие сделки. Время позиции и устаревание +снимка показываются отдельно от календаря выпуска. + +История использует существующий запрос операций всех счетов с точным +`instrumentUid`, включая прошлые годы. Фильтры: все, сделки, купоны, прочее. +Смена фильтра сбрасывает пагинацию; истёкший курсор восстанавливается запросом +первой страницы. Выплаты выпуска не подменяют личные зачисления. + +## Владельцы и восстановление + +`BondController` и `BondService` принадлежат существующему +`TbankConnectionModule`. Все `/api/v1/bonds` endpoints требуют Bearer session +и актуального подключения Т-Банка: поиск, разрешение тикера, карточка по UID, +`quote`, `candles`, `position`, `coupons`, `metrics`. Wire contract принадлежит runtime +OpenAPI, frontend snapshot и types генерируются из него. + +`BondBy`, `FindInstrument` и `GetBondCoupons` используют существующий sparse +Instruments client, сохраняющий отсутствие полей. Рыночные RPC проходят +общие TLS, очередь, rate limit, deadline и retry. Кэш успешных и совместных +незавершённых запросов ограничен LRU и 30 секундами по пользователю, +поколению подключения и параметрам. Ошибки не кэшируются; ошибки авторизации +не подавляются при дополнительной загрузке данных поиска. +Исходный ответ `BondBy` совместно используется поиском, карточкой, котировкой, +графиком, купонами и сводкой показателей в пределах того же пользователя, поколения и TTL. +Одновременная загрузка секций вызывает один `BondBy` на UID. Календарь +и сводка совместно используют один запрос купонов; котировка переиспользуется +при расчёте купонной доходности. MOEX и НКР загружаются только для открытого +выпуска, независимо от поиска. Для 30 новых +результатов поиск выполняет 32 RPC до повторных попыток: один `FindInstrument`, +30 `BondBy` и один пакетный запрос цен. Общий ограничитель backend не заменяет +квоты Т-Банка; при их исчерпании сохраняется `TBANK_UNAVAILABLE`. + +Разделы загружаются независимо, показывают skeleton и локальный retry. +Ошибка фонового обновления сохраняет последние данные. Смена подключения +сбрасывает subtree `/bonds` вместе с остальными финансовыми запросами; +logout использует общий session reset. Общие график, позиция и личные события +переиспользуются страницами акций и облигаций через `instrument-details`. diff --git a/research/bond-credit-ratings.md b/research/bond-credit-ratings.md new file mode 100644 index 0000000..193553a --- /dev/null +++ b/research/bond-credit-ratings.md @@ -0,0 +1,144 @@ +# Источники кредитных рейтингов облигаций + +Проверено 10 сентября 2026 года. Исследование для страницы облигации +WealthLedger: рейтинг агентства, привязанный к конкретному ISIN, без доступа +пользователя к платным информационным системам. Это исследование источников, +а не описание уже реализованной интеграции. + +## Вывод + +Для первого подключения технически подходит публичный +[реестр рейтингов эмиссий НКР](https://ratings.ru/ratings/issues/): один обычный +GET без авторизации возвращает HTML-таблицу с ISIN, рейтингом, датой и ссылкой +на первоисточник. В проверенном ответе 157 уникальных ISIN, включая 36 записей +«рейтинг отозван». Это частичное покрытие одного агентства, а не рейтинг любой +облигации российского рынка. + +У [Эксперт РА](https://raexpert.ru/ratings/debt_inst/) тоже есть открытые +рейтинги выпусков. Однако ISIN находится в отдельных карточках, а список +разбит на страницы. Подключение потребует дополнительных запросов и более +сложного обхода. Официальная структурированная выгрузка агентства оформлена +как [подписка](https://raexpert.ru/all-services/rating-export/). + +Публичное чтение этих страниц проверено. Свободная лицензия на массовое +переиспользование не установлена: НКР и Эксперт РА отдельно ограничивают +распространение своей информации. Это не равнозначно техническому запрету +читать отдельный рейтинг в личном приложении. Перед публичным распространением +базы или коммерческим предоставлением данных нужно отдельно проверить +условия правообладателя. Публичный HTML не даёт контракт API или SLA. + +## НКР: минимальная интеграция + +Источник: . + +В HTML есть `table#issues-table`, затем `tbody > tr`. В каждой проверенной +строке ровно шесть `td`: + +| Индекс | Содержимое | Использование | +| --- | --- | --- | +| 0 | Рейтингуемое лицо, ссылка `/ratings/issuers/.../` | Контекст эмитента | +| 1 | Название эмиссии, ссылка `/ratings/issues/{id}/` | Ссылка на карточку выпуска | +| 2 | Рейтинг, например `AAA.ru`, или `рейтинг отозван` | Значение и статус | +| 3 | ISIN | Точное сопоставление с облигацией | +| 4 | Регистрационный номер | Дополнительный идентификатор | +| 5 | Дата `DD.MM.YYYY`, ссылка на пресс-релиз | Дата действия и источник | + +Например, в ответе на дату проверки: + +| ISIN | Рейтинг | Дата | Источник | +| --- | --- | --- | --- | +| RU000A10EW44 | AAA.ru | 17.04.2026 | [Полюс ПБО-05](https://ratings.ru/ratings/issues/3361/) | +| RU000A1065B1 | Рейтинг отозван | 24.04.2026 | [Реестр эмиссий](https://ratings.ru/ratings/issues/) | + +Эти примеры нужны для проверки структуры, не для постоянного отображения +значений из документа. Прогноза в таблице эмиссий нет. Его нельзя брать из +случайного текста страницы или переносить с рейтинга эмитента. + +Кнопка «Загрузить ещё» обслуживается клиентским DataTables: +[скрипт инициализации](https://ratings.ru/js/initDataTable.js) задаёт +`pageLength` и вызывает `table.page.loadMore()`, без `ajax` или `serverSide`. +В проверенном HTML уже присутствуют все 157 строк таблицы; дополнительные +запросы на каждую карточку для извлечения перечисленных полей не нужны. +Это наблюдение текущей версии сайта, не обещание неизменного формата. + +Предлагаемый подход для личного приложения: + +1. Загружать таблицу на backend с ограничением времени и размера ответа. + Объединять параллельные загрузки и кэшировать общий публичный справочник, + а не запрашивать сайт отдельно для каждого пользователя и ISIN. +2. Проверять заголовки и структуру таблицы; сопоставлять только точный ISIN. + Не угадывать рейтинги по названию, тикеру или группе компаний. +3. Показывать исходное обозначение `AAA.ru`, агентство «НКР», дату действия + и ссылку на карточку или пресс-релиз. +4. Отзыв показывать отдельным статусом. При отсутствии строки говорить + «Нет данных НКР», а не «Рейтинг отсутствует»: другие агентства могут + иметь действующий рейтинг. +5. Недоступность сайта или изменение HTML не должны ломать цену, купоны + и портфель. Время получения данных и дата рейтингового действия имеют + разный смысл; давнюю дату решения нельзя автоматически считать отзывом. + +Это рекомендации реализации, вытекающие из проверенной структуры; +период кэша и обработка ошибок остаются решениями приложения. + +В footer [сайта НКР](https://ratings.ru/ratings/issues/) указано: +«Не допускается распространение настоящей информации любым способом и в любой +форме без предварительного согласия со стороны ООО „НКР“ и ссылки на источник». +Разрешение на публичную перераздачу и стабильный машинный интерфейс этой +проверкой не получены. Авторизация или обход защиты для чтения таблицы +не использовались. + +## Эксперт РА + +В [таблице долговых инструментов](https://raexpert.ru/ratings/debt_inst/) +есть название выпуска, рейтинг, прогноз, дата обновления и ссылки на +пресс-релизы. В проверенном HTML списка ISIN нет. Переход по ссылке +[Россети 001Р-21R](https://raexpert.ru/database/securities/bonds/1000074695/) +даёт реквизит `ISIN = RU000A10G148` и таблицу `.object-rating-table` +с `ruAAA` от `09.09.2026`. + +Список использует серверную пагинацию: `setRatingPageHash(...)` отправляет +запрос на `/ratings/index/ajax-set-rating-page-hash/`. Поэтому одного GET +списка недостаточно для полного справочника. Не следует строить массовый +сканер карточек только ради добавления второго агентства в первую версию. + +Официальная [услуга выгрузки](https://raexpert.ru/all-services/rating-export/) +описывает SOAP, REST и Excel как виды подписки. REST использует уникальный +код доступа; документация содержит методы получения рейтинг-листа и +рейтинговых действий, параметры даты и отозванных рейтингов. Наличие кнопки +«Экспорт» на публичной странице не доказывает бесплатность выгрузки. + +В footer агентство требует предварительное согласие и ссылку на источник +для распространения информации. Как и для НКР, возможность чтения карточки +не следует представлять как открытую лицензию на распространение базы. + +## Другие проверенные варианты + +| Источник | Проверенный результат | Вывод для первого подключения | +| --- | --- | --- | +| [MOEX ISS CCI](https://iss.moex.com/iss/reference/781) | Официально документирован список актуальных рейтингов выпусков. Проверенный анонимный запрос `/iss/cci/rating/securities.json?iss.meta=off` вернул пустое тело | Не считать пустое тело валидным отсутствием рейтингов; пригодный ответ и условия доступа не подтверждены | +| [MOEX CCI: выпуск компании](https://iss.moex.com/iss/reference/867) | Документирован путь с ID компании и ISIN, пример `1113/RU000A1051U1` | Документация существует, но рабочая бесплатная интеграция не доказана | +| [АКРА](https://www.acra-ratings.ru/ratings/) | `/ratings/`, `/ratings/issuers/` и главная страница вернули HTTP 403 в проверенной среде | Не обходить ограничение; доступный источник данных здесь не подтверждён | +| [НРА](https://www.ra-national.ru/ratings/) | Публичный HTML содержит реестр и названия облигационных выпусков, но точный ISIN в проверенном списке не обнаружен | Нужна отдельная проверка карточек и условий; сейчас менее прямой путь, чем НКР | +| [Cbonds API](https://cbonds.ru/api/) | Официальный продукт включает кредитные рейтинги, публичную демоверсию и индивидуальный доступ. Цена зависит от набора данных | Вариант структурированной интеграции после получения доступа; демо не означает бесплатную полную базу | +| [Интерфакс / RusBonds](https://www.rusbonds.ru/) | Запрос вернул страницу JavaScript challenge, а не данные облигаций | Рабочий анонимный источник рейтингов и условия API этой проверкой не установлены | + +Наблюдаемый HTTP 403, challenge или пустой ответ не доказывают отсутствие +данных у провайдера. Это ограничения конкретной проверки; они не дают +основания выдумывать поля API, доступ или цены. + +## Предметные границы + +[Банк России](https://www.cbr.ru/finm_infrastructure/ra/) различает мнение +агентства о способности организации исполнять обязательства и оценку риска +отдельного финансового обязательства или инструмента. Поэтому рейтинг +эмитента не должен молча становиться рейтингом любой его облигации. + +При добавлении других агентств следует сохранять агентство и исходную шкалу: +`ruAAA`, `AAA.ru` и `AAA(RU)` нельзя свести к одному безымянному `AAA`. +Банк России отдельно публикует +[сопоставление российских шкал](https://www.cbr.ru/press/pr/?file=638557747357168625INFR.htm), +что не отменяет необходимость показывать происхождение оценки. + +Стабильный открытый API с проверенным полным покрытием всех агентств в рамках +этого исследования не найден. Публичный реестр НКР даёт практически полезное +начало без покупки доступа и без подмены рейтинга уровнем риска брокера.