Files
specs/openspec/changes/account-top-positions/specs/portfolio-view/spec.md
T

23 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Нормализованный состав Portfolio

Система MUST возвращать только обычные Portfolio Position с ненулевым количеством и доступные Cash Balance с ненулевой суммой. Cash Balance MUST быть отдельным от Portfolio Position элементом и агрегироваться до одного значения на валюту. Portfolio Position с типом CURRENCY MUST означать позицию по торгуемому валютному Instrument и MUST NOT смешиваться с Cash Balance. Порядок Portfolio Position и Cash Balance MUST соответствовать upstream-порядку; система MUST NOT сортировать или объединять Portfolio Position с одинаковым FIGI. Виртуальные позиции и заблокированные денежные остатки MUST NOT входить в Portfolio.

Каждая Portfolio Position MUST содержать нормализованный тип STOCK, BOND, FUND, CURRENCY, FUTURE, OPTION, STRUCTURED_NOTE, DIGITAL_FINANCIAL_ASSET либо OTHER, строковое quantity, nullable currentValue, nullable averagePurchasePrice, nullable absoluteReturn, nullable isin, nullable ticker и nullable logoName — имя файла логотипа бренда инструмента, nullable sector — сектор инструмента и nullable issuer — эмитент позиции. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть null. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Поле sector позиции типа STOCK, BOND или FUND MUST содержать значение сектора из metadata инструмента в том виде, как его определяет T-Bank, без перекодирования и собственной классификации, с обрезанными пробелами в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть null. Поле issuer позиции типа STOCK, BOND или FUND, включая фонд, MUST быть объектом с непустыми строками id — идентификатор бренда актива инструмента — и name — название этого бренда (не название компании), когда T-Bank предоставляет для инструмента бренд актива с идентификатором и названием, непустыми после обрезки пробелов в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть null. Система MUST NOT определять эмитента по совпадению названия, тикера или ISIN. Сектор и эмитент не влияют на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot: их отсутствие либо недоступность MUST NOT делать Snapshot неуспешным или устаревшим и MUST NOT скрывать позицию или её финансовые значения, кроме отказа token по требованию «Свежесть и fallback полного Portfolio Snapshot». Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения.

Scenario: Позиция с неизвестным upstream-типом

  • WHEN upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента
  • THEN система возвращает её в исходном порядке с типом OTHER

Scenario: Позиция валютного инструмента не является денежным остатком

  • WHEN upstream возвращает ненулевую Portfolio Position валютного инструмента и ненулевой Cash Balance той же валюты
  • THEN система возвращает Portfolio Position с типом CURRENCY отдельно от Cash Balance и не объединяет их между собой

Scenario: Позиция без metadata инструмента

  • WHEN lookup metadata по FIGI не дал ticker или name либо временно недоступен
  • THEN система возвращает позицию с FIGI как fallback-идентификатором и всеми доступными финансовыми значениями

Scenario: Имя инструмента доступно

  • WHEN metadata инструмента по FIGI содержит непустое name
  • THEN система возвращает name как отображаемый идентификатор позиции вместо FIGI или ticker

Scenario: FIGI не разрешается, но доступен instrument UID

  • WHEN FIGI позиции не даёт metadata, а её instrument UID возвращает непустое name
  • THEN система возвращает name, полученное по instrument UID

Scenario: ISIN инструмента доступен

  • WHEN metadata инструмента позиции содержит непустой isin
  • THEN система возвращает его в поле isin позиции

Scenario: ISIN инструмента отсутствует

  • WHEN у инструмента позиции нет ISIN либо metadata недоступна
  • THEN система возвращает позицию с isin: null, не скрывая финансовые значения

Scenario: Тикер инструмента доступен

  • WHEN metadata инструмента позиции содержит непустой ticker
  • THEN система возвращает его в поле ticker позиции

Scenario: Тикер инструмента отсутствует

  • WHEN у инструмента позиции нет ticker либо metadata недоступна
  • THEN система возвращает позицию с ticker: null, не скрывая финансовые значения

Scenario: Имя файла логотипа бренда доступно

  • WHEN metadata инструмента позиции содержит бренд с непустым именем файла логотипа
  • THEN система возвращает его в поле logoName позиции

Scenario: Имя файла логотипа бренда отсутствует

  • WHEN у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна
  • THEN система возвращает позицию с logoName: null, не скрывая финансовые значения

Scenario: Денежные остатки в одной валюте

  • WHEN upstream возвращает несколько ненулевых доступных денежных остатков одной валюты
  • THEN система возвращает один Cash Balance с их точной суммой

Scenario: Штатный пустой Portfolio

  • WHEN у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков
  • THEN система отвечает 200 с пустыми positions, cashBalances и totalsByCurrency

Scenario: Сектор инструмента доступен

  • WHEN metadata инструмента позиции содержит сектор energy
  • THEN система возвращает в поле sector позиции значение energy без изменений

Scenario: Сектор инструмента отсутствует

  • WHEN у инструмента позиции нет сектора, сектор пустой либо metadata недоступна
  • THEN система возвращает позицию с sector: null, не скрывая финансовые значения

Scenario: Эмитент позиции доступен

  • WHEN T-Bank предоставляет для инструмента позиции бренд актива с идентификатором brand-1 и названием «Сбер»
  • THEN система возвращает в поле issuer позиции идентификатор brand-1 и название «Сбер»

Scenario: Название эмитента — название бренда

  • WHEN T-Bank предоставляет для инструмента бренд актива с названием «Сбер» и компанией «ПАО Сбербанк»
  • THEN система возвращает в issuer.name «Сбер»

Scenario: Эмитент у фонда

  • WHEN T-Bank предоставляет для инструмента позиции FUND бренд актива с идентификатором brand-2 и названием «Фонд»
  • THEN система возвращает в issuer позиции идентификатор brand-2 и название «Фонд»

Scenario: Пробелы по краям обрезаются

  • WHEN T-Bank возвращает сектор " energy " и название бренда " Сбер "
  • THEN система возвращает sector energy и issuer.name «Сбер»

Scenario: Значение из одних пробелов

  • WHEN T-Bank возвращает сектор " " либо название бренда " "
  • THEN система возвращает sector: null либо issuer: null соответственно

Scenario: Остальные типы позиций

  • WHEN T-Bank предоставляет сектор и бренд актива для инструмента позиции типа FUTURE
  • THEN система возвращает у этой позиции sector: null и issuer: null

Scenario: Эмитент позиции отсутствует

  • WHEN у инструмента позиции нет бренда актива, идентификатор либо название бренда пустые или данные бренда недоступны
  • THEN система возвращает позицию с issuer: null, не скрывая финансовые значения

Scenario: Эмитент не определяется по названию

  • WHEN две позиции имеют одинаковое название, но разные идентификаторы бренда актива
  • THEN система возвращает в issuer позиций разные идентификаторы бренда и не объединяет их

Scenario: Недоступность сектора и эмитента не нарушает Snapshot

  • GIVEN сектор и бренд актива позиции ещё ни разу не получены
  • WHEN получение сектора и бренда актива завершается ошибкой, а количества и цены позиции получены
  • THEN система сохраняет Snapshot с sector: null и issuer: null у затронутых позиций; их currentValue, totalsByCurrency, lastSuccessfulAt и isStale такие же, как при доступных сектора и бренде

Requirement: Свежесть и fallback полного Portfolio Snapshot

Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST возвращать { data, meta }, где data является сохранённым Snapshot, а meta соответствует наблюдаемому состоянию resource по tbank-data-sync, читать его без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio, даже если Snapshot отсутствует или старше 60 минут; обновлением владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обхода MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность или свежесть финансового Snapshot, кроме отказа token при получении сектора или бренда актива позиции: он MUST обрабатываться так же, как отказ token при обходе Portfolio (исключение, источник — #48). Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника.

Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента. Исключение: сектор и бренд актива позиции MUST храниться как последнее известное значение и обновляться по требованию «Обновление сектора и эмитента» (#48).

Scenario: Свежий Snapshot

  • WHEN User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
  • THEN система возвращает свежий Snapshot без нового обращения к T-Bank

Scenario: Медленный lookup инструмента

  • WHEN сведения об инструменте ещё запрашиваются онлайн
  • THEN система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity

Scenario: Fallback после временной upstream-ошибки

  • WHEN часовой обход Portfolio неуспешен из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
  • THEN система отвечает 200 этим Snapshot с isStale: true и исходным lastSuccessfulAt

Scenario: Недействительный token не маскируется fallback-данными

  • WHEN T-Bank отклоняет token как неаутентифицированный или без прав при часовом обходе Portfolio
  • THEN система не возвращает fallback Snapshot и отвечает 400 Problem Details с code INVALID_TBANK_TOKEN

Scenario: Неполная оценка при новом составе

  • WHEN успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
  • THEN новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции без подстановки нуля

Scenario: Сектор и бренд читаются из хранимого значения

  • GIVEN сектор позиции получен 10 часов назад
  • WHEN User запрашивает Portfolio
  • THEN система возвращает хранимый sector без обращения к T-Bank за сектором

Scenario: Отказ token при получении бренда

  • WHEN T-Bank отклоняет token как неаутентифицированный или без прав при получении сектора или бренда актива позиции
  • THEN система не возвращает fallback Snapshot и отвечает 400 Problem Details с code INVALID_TBANK_TOKEN, как при отказе token в обходе Portfolio

ADDED Requirements

Requirement: Обновление сектора и эмитента

Система SHALL обновлять сектор и бренд актива позиции при первом обходе по hourly-investment-sync, начавшемся не ранее чем через 24 часа после последнего успешного получения; в остальных обходах значение не меняется. Пока значение не получено, получение повторяется при каждом обходе.

Источник (ненормативно): #48, #48

Scenario: Обход раньше 24 часов

  • GIVEN бренд позиции получен в понедельник в 09:00, а затем T-Bank сменил его название
  • WHEN обход начинается во вторник в 08:59, то есть через 23 ч 59 мин
  • THEN issuer.name остаётся прежним

Scenario: Обход через 24 часа

  • GIVEN бренд позиции получен в понедельник в 09:00, а затем T-Bank сменил его название
  • WHEN обход начинается во вторник в 09:00
  • THEN issuer.name новое

Requirement: Результат получения сектора и эмитента

Результат получения SHALL определять поле позиции и отсчёт 24 часов по таблице; совпадает ровно одна строка.

Исход получения Поле Отсчёт 24 часов
Получено значение Новое значение Начинается заново
Ответ «не найдено» или значения нет null Начинается заново
Ответ о другом инструменте или активе Прежнее значение или null Не меняется; получение неуспешно
Иная ошибка, кроме отказа token Прежнее значение или null Не меняется; получение неуспешно

Неуспешное получение SHALL NOT мешать обновлению остальных данных Portfolio; отказ token — по «Свежесть и fallback полного Portfolio Snapshot».

Источник (ненормативно): #48

Scenario: Ответ без значения заменяет известное значение

  • GIVEN у позиции известен sector energy, 24 часа прошли
  • WHEN T-Bank отвечает, что сектора у инструмента нет
  • THEN sector: null

Scenario: «Не найдено» перезапускает отсчёт

  • GIVEN у позиции известен sector energy, 24 часа прошли, и обход получает от T-Bank «не найдено»
  • WHEN следующий обход начинается через час, а T-Bank уже возвращает сектор financial
  • THEN sector остаётся null; значение financial появляется при первом обходе, начавшемся не ранее чем через 24 часа после ответа «не найдено»

Scenario: Ответ о другом инструменте

  • GIVEN у позиции известен sector energy, 24 часа прошли
  • WHEN T-Bank возвращает данные другого инструмента
  • THEN sector остаётся energy, а следующий обход повторяет получение

Scenario: Недоступность T-Bank сохраняет известное значение

  • GIVEN у позиции известен sector energy, 24 часа прошли
  • WHEN получение сектора завершается недоступностью T-Bank, а цены и количества получены
  • THEN позиция возвращается с sector energy, остальные данные Portfolio обновлены, lastSuccessfulAt новый

Scenario: Значение ещё не известно

  • GIVEN новая позиция, сектор которой ни разу не получен успешно
  • WHEN получение сектора неуспешно при одном обходе и успешно при следующем
  • THEN после первого обхода sector: null, после следующего значение сектора