diff --git a/AI_INDEX.md b/AI_INDEX.md index 4b64027..3e63df3 100644 --- a/AI_INDEX.md +++ b/AI_INDEX.md @@ -51,6 +51,7 @@ 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) | | Позиции/суммы портфеля, 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/brokerage-account-ledger.md b/features/brokerage-account-ledger.md index 68b265d..aa1d808 100644 --- a/features/brokerage-account-ledger.md +++ b/features/brokerage-account-ledger.md @@ -22,6 +22,8 @@ sequenceDiagram `GET /api/v1/brokerage-accounts/:accountId/operations` требует Bearer access token. Необязательные `from` и `to` задают включительный период; без периода запрос начинается с 1 января текущего года. `categories` принимает `trade`, `income`, `tax`, `fee`, `transfer`, `other`; `cursor` и `limit` управляют cursor-пагинацией. +Необязательный `instrumentUid` фильтрует историю одной бумаги через upstream и включает историю с открытия доступных счетов, если `from` не задан. Для сделок `quantity` содержит исполненное `quantityDone`; если его нет, значение `null`. Дата остаётся датой операции. Подробности и ограничения — [страница акции](stock-detail.md). + Ответ передаёт `accountId`, `items`, `nextCursor`, `hasNext` и `observedAt`. Каждая `Ledger Entry` соответствует ровно одной операции T-Bank и содержит её тип, категорию, описание, дату, доступный инструмент, nullable `logoName`, `payment`, `quantity`, `price`, `accruedInterest` и `commission`. `logoName` — presentation metadata: если она недоступна, это не меняет запись журнала. Деньги передаются decimal strings в исходной ISO-валюте. Frontend только форматирует значения. Комиссия сделки остаётся деталью той же записи; операции без `payment` отображаются как «Без движения денег». @@ -36,7 +38,7 @@ filters и данные сохраняют существующую семант - Запрашиваются только исполненные операции T-Bank. - Полный журнал фильтруется по периоду (два DateInput «С»/«По» с форматом DD.MM.YYYY и min/max-ограничением, русская локаль календаря) и категориям (MultiSelect с мультивыбором из trade, income, tax, fee, transfer, other). Фильтры имеют черновик в контролах и применённое состояние: изменение черновика не вызывает новый запрос к серверу; «Применить» коммитит черновик и сбрасывает cursor-пагинацию на первую страницу; «Сбросить» очищает черновик и немедленно применяет пустой фильтр (период по умолчанию и все категории). Cursor-пагинация («Назад»/«Далее») работает по последнему применённому фильтру, не по черновику. - В полном журнале и пяти последних записях обзора одна и та же компактная строка показывает логотип инструмента либо буквенный fallback, описание операции, дату, инструмент и денежное движение. Денежная метка строки показывает подпись конкретного типа события: для income-записи — «Купон» или «Дивиденд» (по upstream-типу купона или дивиденда), для остальных — подпись категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Подпись типа выводится один раз и не повторяется во вторичной метадате (дата · инструмент); направление передаётся знаком «+» / «−` и семантическим цветом, а не текстом. Записи без движения денег остаются нейтральными («Без движения денег»). -- Полный журнал не показывает цену, количество, НКД и комиссию записи. +- Полный журнал не показывает цену, количество, НКД и комиссию записи. Страница акции переиспользует тот же `LedgerEntryRow` с дополнительными количеством, ценой и ссылкой на счёт. Даты имеют формат ДД.ММ.ГГГГ, время операций явно обозначено UTC. При отсутствии описания используется локализованный тип операции. - Backend хранит в памяти процесса до 100 страниц Account Ledger: свежая страница живёт до 30 секунд, а при переполнении вытесняется давно не использовавшаяся страница (LRU). Ключ включает непрозрачную generation текущего T-Bank connection, пользователя, счёт, нормализованные период и categories, cursor и limit; отсутствующий `to` приводится к текущему 30-секундному bucket. Проверка connection и доступа к счёту выполняется до выдачи кэшированной страницы. После успешного создания, замены или удаления connection сохранённая либо поздно завершившаяся страница предыдущей generation недоступна Operations, Income Summary и Income Analytics. Старые страницы удаляются обычными TTL/LRU limits; операции не сохраняются и не синхронизируются локально. - Журнал не рассчитывает остаток после операции, суммарные доходы или налоги, историческую стоимость Portfolio, конвертацию валют и рыночные изменения цены. - `409 TBANK_NOT_CONNECTED`, `400 INVALID_TBANK_TOKEN`, `404` недоступного счёта и `503 TBANK_UNAVAILABLE` используют существующую семантику ошибок T-Bank. diff --git a/features/brokerage-accounts.md b/features/brokerage-accounts.md index 89b5bb9..0318f52 100644 --- a/features/brokerage-accounts.md +++ b/features/brokerage-accounts.md @@ -42,7 +42,9 @@ sequenceDiagram "totalsByCurrency": [ { "currency": "RUB", "value": "12500.50" }, { "currency": "USD", "value": "40" } - ] + ], + "stockPositionCount": 5, + "bondPositionCount": 12 } } ] @@ -52,6 +54,8 @@ sequenceDiagram `portfolioSummary` — nullable наблюдение текущего портфеля конкретного счета. `totalsByCurrency` точно складывает ненулевые текущие стоимости позиций и денежные остатки только в одинаковой исходной ISO-валюте; FX-конвертация и единый итог не выполняются. Успешный пустой портфель возвращает пустой массив. Когда T-Bank временно недоступен только для summary одного счета, этот счет всё равно остаётся в списке с `portfolioSummary: null`; переход к его деталям и остальные карточки не блокируются. +`stockPositionCount` и `bondPositionCount` — число ненулевых обычных позиций акций и облигаций по наименованиям, а не сумма штук или лотов. Подсчёт использует уже полученный `GetPortfolio`, без дополнительных запросов metadata; включает дробные и короткие позиции, даже если текущая стоимость неизвестна. Нулевые и виртуальные позиции, фонды, валютные и прочие инструменты не входят в эти два счётчика. Успешный пустой портфель возвращает оба счётчика равными нулю; недоступная сводка остаётся `null`. + Список является результатом текущего наблюдения T-Bank connection: если счёт больше не открыт, не поддерживается или недоступен через это подключение, он не считается доступным для WealthLedger. Ошибки connection или token, а также ошибки исходного получения списка, не превращаются в частичный ответ. ## Состояния @@ -67,7 +71,7 @@ sequenceDiagram - Неизвестная ошибка: экран предлагает только повторить загрузку. - Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов. -Карточка ведёт на `/accounts/:accountId` по всей области и сохраняет нативную клавиатурную навигацию/focus. Она показывает название, локализованный тип, до двух итогов в исходных валютах, число остальных валют, дату открытия без времени и видимое действие открытия. Недоступная или пустая summary отображается локально на карточке. +Карточка ведёт на `/accounts/:accountId` по всей области и сохраняет нативную клавиатурную навигацию/focus. На широком экране карточки расположены в две колонки, на узком — в одну. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая summary отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная summary не подменяет их нулями. Hover и видимый keyboard focus подчёркивают переход; reduced motion отключает движение. ## Ограничения MVP diff --git a/features/portfolio-view.md b/features/portfolio-view.md index 19f4da6..7674bdc 100644 --- a/features/portfolio-view.md +++ b/features/portfolio-view.md @@ -35,7 +35,7 @@ sequenceDiagram Все количества и суммы передаются decimal strings без экспоненциальной записи. Деньги передаются как Money `{ currency, value }` с ISO-кодом валюты. Backend выполняет точные операции через `BigInt`; frontend форматирует строки для русской локали и не выполняет финансовую арифметику кроме display-only оценки доли позиции. -`Portfolio Position` -- ненулевой обычный актив из T-Bank portfolio. Position с type `CURRENCY` означает торгуемый валютный Instrument и не является Cash Balance. `currentValue` равен количеству, умноженному на текущую цену, когда оба значения и валюта доступны. `absoluteReturn` доступен только при одинаковой известной валюте текущей и средней цены. `isin` и `ticker` берутся из уже разрешённой metadata Instrument и передаются nullable-строками. Невычислимые значения передаются `null`, а не нулём. Позиции остаются в upstream-порядке; виртуальные позиции не входят в ответ. +`Portfolio Position` -- ненулевой обычный актив из T-Bank portfolio. Position с type `CURRENCY` означает торгуемый валютный Instrument и не является Cash Balance. `currentValue` равен количеству, умноженному на текущую цену, когда оба значения и валюта доступны. `absoluteReturn` доступен только при одинаковой известной валюте текущей и средней цены. `instrumentUid` передаётся из upstream для точной идентификации и перехода на [страницу акции](stock-detail.md). `isin` и `ticker` берутся из уже разрешённой metadata Instrument и передаются nullable-строками. Невычислимые значения передаются `null`, а не нулём. Позиции остаются в upstream-порядке; виртуальные позиции не входят в ответ. `Cash Balance` -- доступный ненулевой денежный остаток. Несколько остатков одной валюты суммируются; заблокированные средства не включаются. Итоги группируются по валюте и включают стоимость позиций и Cash Balance без конвертации. diff --git a/features/stock-detail.md b/features/stock-detail.md new file mode 100644 index 0000000..967642b --- /dev/null +++ b/features/stock-detail.md @@ -0,0 +1,162 @@ +# Страница акции + +Авторизованный пользователь открывает `/stocks/SBERP` по тикеру. Старые +адреса `/stocks/:uid` продолжают работать. Сервер возвращает `slug`: проверенный +однозначный тикер либо исходный UID. При открытии UUID-ссылки страница заменяет +адрес на тикер только при совпадении UID, сохраняя период и фильтр событий. +Прямое открытие и обновление страницы с тикером сначала разрешает его в UID; +цены, позиция и личные операции затем запрашиваются только по этому UID. +Ссылки находятся в названиях и логотипах акций отдельного портфеля и обзора +«Все счета». Названия сохраняют обычный цвет текста; буквенная заглушка +логотипа также ведёт на страницу акции. Карточки получают проверенный `slug` +через существующий запрос акции по UID: после его загрузки оба `href` сразу +содержат тикер. Одинаковые UID в разных счетах используют один запрос и общий +кэш страницы акции. До загрузки и при ошибке остаётся рабочая ссылка по UID; +загрузка адресов не блокирует портфель. +Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: поиск +по названию или тикеру через каталог Т-Банка, начиная с двух символов. +Результаты разных выпусков, включая обыкновенные и привилегированные акции, +имеют отдельные UID. Карточка результата показывает логотип, последнюю цену +с её временем, тип бумаги, площадку, код режима, ISIN и доступность торгов через +API. Поиск показывает одну карточку на нормализованный ISIN. До ограничения +в 30 результатов выбирается представитель выпуска: доступность через API, +точное совпадение тикера, затем режим TQBR; равные варианты упорядочены по UID. +Записи без ISIN сохраняются отдельно по UID. Обыкновенные и привилегированные +акции с разными ISIN не объединяются. Цена и ссылка принадлежат выбранному +UID; данные разных режимов не смешиваются. Прямые ссылки из портфеля сохраняют +исходный UID, даже если поиск выбрал другой режим этого выпуска; такая страница +сохраняет UUID в адресе. Если один точный тикер относится к разным выпускам, +сервер возвращает `409 STOCK_TICKER_AMBIGUOUS`, а страница предлагает поиск. +Поиск тикера нечувствителен к регистру, но использует точное совпадение: +SBER не заменяется на SBERP. Однобуквенные тикеры также поддерживаются. +Прямая ссылка не требует предварительного открытия портфеля. Хлебные крошки +«Акции → название» возвращают к поиску. + +## Содержание и состояния + +Верхний блок объединяет название, тип акции, текущую цену и показатели компании. +Денежные показатели используют символ валюты; описание компании видно сразу +под логотипом и названием, перед ценой. Далее идут график, моя позиция, предстоящие +дивиденды и личная история событий. +Логотип использует существующий CDN helper и буквенный fallback. +Позиция отсутствующей или полностью проданной бумаги не создаёт пустого блока; +рыночные данные и личная история остаются доступны. + +Каждый раздел загружается независимо. Первичная загрузка показывает structural +skeleton, ошибка — локальную повторную попытку. При ошибке фонового обновления +сохраняются предыдущие данные и предупреждение. Цена содержит собственное время +UTC, которое может быть старше времени получения ответа. Статус торгов не +выводится из свежести цены. Отсутствующее значение не заменяется нулём. + +## API и владельцы + +Все методы требуют Bearer session и текущее подключение Т-Банка. +Новые endpoints принадлежат `StockController` / `StockService` существующего +`TbankConnectionModule`: + +| Метод | Содержимое | +| --- | --- | +| `GET /api/v1/stocks?query=...` | До 30 результатов с признаками выпуска/режима, логотипом и последней ценой. | +| `GET /api/v1/stocks/:uid` | Точный инструмент: название, тикер, `slug`, тип, сектор, лот, валюта, начало дневной истории. | +| `GET /api/v1/stocks/by-ticker/:ticker` | Разрешение точного тикера и карточка выбранного выпуска с UID. | +| `GET /api/v1/stocks/:uid/quote` | Последняя цена и время, статус, изменение и явно указанная база. | +| `GET /api/v1/stocks/:uid/candles?period=...` | Дневные цены для `month`, `halfYear`, `year`, `all`. | +| `GET /api/v1/stocks/:uid/position` | Точная сводка открытой позиции и распределение по счетам. | +| `GET /api/v1/stocks/:uid/dividends` | События компании за пять прошлых лет и следующий год, личные оценки. | +| `GET /api/v1/stocks/:uid/company` | Доступное описание и финансовый срез. | + +Личная история использует существующий +`GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`. +Фильтр UID валидируется и передаётся upstream как `instrumentId`; ответ также +проверяется по UID. Без `from` такой запрос начинается с самой ранней известной +даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch. +Обычный журнал без фильтра инструмента сохраняет начало текущего года. +UID включён в ключ кэша и подпись общего курсора: курсор другой бумаги, +пользователя, поколения подключения или фильтра отклоняется. Истечение курсора +восстанавливается переходом на первую страницу с повторным запросом. + +Сервис портфеля остаётся владельцем снимков и оценки позиций. Новая сводка +фильтрует `STOCK` по точному UID и использует decimal/BigInt, без FX. +Средняя цена равна сумме количеств, умноженных на средние цены, делённой на +общее количество; она округляется сервером до девяти знаков, проценты — до шести. +Неполные или разновалютные денежные итоги недоступны. Для нулевого количества +и сочетаний длинных/коротких позиций общая средняя и процент не синтезируются. +Результат открытой позиции исключает продажи и дивиденды и не называется полной +доходностью. Время и `isStale` сохраняют семантику исходного Portfolio Snapshot. + +## Рыночная цена и график + +Изменение рассчитывается относительно последней завершённой дневной свечи +строго до календарной даты последней цены по Москве. Поиск базы охватывает +20 дней до последней цены; если подходящей свечи нет, сравнение недоступно. +Дата базы указана рядом с изменением. `GetClosePrices` не используется: +его основное закрытие может относиться к текущему дню. + +График показывает дневные `close`, сохраняет `isComplete` и предупреждает +о формирующейся свече. Окна RPC не превышают 365 дней, общие границы +дедуплицируются. «Вся история» начинается с `first_1dayCandleDate`; если источник +не сообщает начало истории, этот период недоступен, остальные работают. +Пустое окно до начала торгов не считается ошибкой. Линия текущей средней +покупки доступна только в валюте графика; шкала включает её значение. +Таблица цен раскрывается под графиком: новые даты первыми, валюта в цене, +статус завершённости свечи, закреплённая шапка и прокрутка с клавиатуры. +Стили графиков подключены на уровне приложения и каталога, поэтому оформление +подсказки не зависит от ранее посещённых страниц. + +Метки старых сделок и дивидендов на графике не добавлены: соответствие шкалы +после корпоративных изменений не подтверждено. Лента использует дату операции; +количество сделки берётся из `quantityDone`, а не порученного `quantity`. +Отсутствующее исполненное количество не подменяется порученным. Сами исполнения +не превращаются в точные точки графика. + +## Дивиденды и финансовые показатели + +События компании отделены от личных зачислений. Известные будущие выплаты +определяются по `paymentDate`, при её отсутствии — по `recordDate`; прошедшие +и недатированные события помещены в отдельную историю компании. Отменённые +события исключаются. Личная оценка умножает сумму на акцию на текущее положительное +количество. Для уже прошедшего реестра оценка не строится. Время позиции и +предупреждение о stale/недоступной оценке показаны отдельно. + +Оценка не доказывает право на выплату, не учитывает будущие сделки и не обещает +индивидуальную дату зачисления. Статусы «рекомендован/утверждён», сумма отдельного +зачисления после налога и налоговый смысл `dividend_net` не выводятся из API. +Пустой список означает отсутствие известных событий только за указанный период. + +`GetAssetFundamentals` декодируется отдельным Instruments client с +`defaults: false`: отсутствующие поля не превращаются в нули. Остальные +существующие клиенты сохраняют прежнюю настройку декодирования. Финансовый сектор +показывает капитализацию, P/E, P/B, ROE и чистую прибыль; остальные — капитализацию, +P/E, выручку, EBITDA, чистую прибыль, общий долг и чистый долг / EBITDA, если +значения доступны. Денежные показатели без валюты не показываются. +TTM означает последние 12 месяцев, MRQ — последний отчётный квартал; +время получения не выдаётся за дату отчётности. Годовых финансовых рядов нет. + +## Границы и проверка + +Новые RPC проходят существующий `TbankClient`: TLS, rate limit, очередь, +deadline и retry. Доступ/расшифровка остаются у `TbankAccessService` и +`TbankTokenService`. Успешные ответы и совместные незавершённые запросы кэшируются +в bounded LRU на 30 секунд по пользователю, поколению подключения и параметрам. +Для поиска `FindInstrument` дополняется сведениями `ShareBy` через тот же +кэш карточки инструмента и одним пакетным `GetLastPrices` для списка. Ошибка +дополнительной загрузки или исчезнувшая карточка не скрывает исходную запись +каталога; недоступные логотипы и цены имеют fallback. Ошибки авторизации +не подавляются. +Ошибка целого запроса не кэшируется; отсутствие дополнительного источника +обозначается в ответе. Cache subtree `/stocks` сбрасывается вместе с финансовыми +данными при изменении подключения; logout использует существующий session reset. + +Контракт генерируется из runtime Nest OpenAPI, затем обновляются frontend snapshot +и generated types. Unit/HTTP/MSW tests проверяют точность, разделение инструментов, +фильтрацию и курсоры, валидацию, отсутствие позиции и локальные ошибки. +Браузерная проверка использует синтетические ответы и широкий/узкий viewport. +Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и +временные зоны расчётов не меняются. Личная история использует общий с портфелем +`LedgerEntryRow`, дополненный количеством, ценой и ссылкой на счёт. История +дивидендов компании раскрывается в оформленном блоке с отдельными карточками +выплат, датой реестра, доступной датой выплаты и суммой на акцию. +Реальные ограничения источника описаны в +[результатах исследования](../../design/stock-detail-api-live-findings.md). +История открытых счетов не обещает полную историю владения, включая закрытые +счета, переводы и универсальные корпоративные события. diff --git a/features/tbank-connection.md b/features/tbank-connection.md index c7b1baf..584c1db 100644 --- a/features/tbank-connection.md +++ b/features/tbank-connection.md @@ -36,7 +36,7 @@ API, logs или errors. Поэтому сохранённый или поздн После успешного `PUT` или `DELETE` frontend сначала коммитит подтверждённый статус connection, затем сбрасывает все generated GET query в -`/api/v1/brokerage-accounts` subtree. Активные выборки запрашиваются заново, а +`/api/v1/brokerage-accounts` и `/api/v1/stocks` subtrees. Активные выборки запрашиваются заново, а неактивные загружаются при следующем использовании; данные предыдущей границы не остаются Client Cached Snapshot. Неуспешная mutation не меняет status, backend generation или financial cache. Обычная ошибка фонового refetch @@ -58,7 +58,7 @@ FIFO-ограничитель на экземпляр запускает не б ## Ограничения Подключение используется поставленными сценариями Brokerage Accounts, -Portfolio, Account Ledger и Future Cash Events; frontend не обращается к +Portfolio, Account Ledger, Future Cash Events и [страницы акции](stock-detail.md); frontend не обращается к T-Bank напрямую. Token encryption key не ротируется, rate limit остаётся per-instance, а обновление connection не сериализуется между распределёнными экземплярами backend. Вендорные protobuf-контракты взяты из