docs: document hourly investment reads
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 13s
Conventional commits / commits (pull_request) Successful in 12s
Docs security / security (pull_request) Successful in 8s
Docs verify / verify (pull_request) Successful in 14s

This commit is contained in:
2026-10-09 19:56:40 +03:00
parent 3a272681c5
commit a45d3e77b9
6 changed files with 154 additions and 121 deletions
@@ -1,6 +1,8 @@
# 017. Локальное чтение данных T-Bank и фоновая синхронизация # 017. Локальное чтение данных T-Bank и фоновая синхронизация
**Status:** Accepted — архитектурное направление согласовано. **Status:** Accepted — историческое архитектурное направление. Решения ниже о
request-driven запуске, ручном обновлении и отсутствии периодического обхода
заменены [ADR 019](019-hourly-reads-and-online-instrument-reference.md).
Prefetch policy ниже заменена [ADR 018](018-minimal-runtime-coordination.md): Prefetch policy ниже заменена [ADR 018](018-minimal-runtime-coordination.md):
свечи загружаются по запрошенному диапазону, без предварительной загрузки свечи загружаются по запрошенному диапазону, без предварительной загрузки
@@ -1,25 +1,37 @@
# 019. Часовой обход и онлайн-справочник инструментов # 019. Часовой обход и онлайн-справочник инструментов
**Status:** Accepted. Дополняет [ADR 017](017-local-tbank-reads-and-background-sync.md) **Status:** Accepted. Заменяет request-driven, manual-refresh и
и [ADR 018](018-minimal-runtime-coordination.md). no-periodic-walk части [ADR 017](017-local-tbank-reads-and-background-sync.md)
и дополняет [ADR 018](018-minimal-runtime-coordination.md).
## Decision ## Decision
- Личные T-Bank данные обновляются владельцами источников через durable hourly - Личные T-Bank данные текущего подключения обновляются владельцами источников
walk; обычные GET не запускают screen import. через durable hourly walk; его результат ограничен этой connection boundary.
- Quotes, candles, operations, forecasts и события сохраняют собственные Обычный GET и retry читают только сохранённый результат и не запускают
локальные состояния и время последнего успешного чтения. personal work.
- Только первое открытие неподтверждённого исторического интервала operations
и первого неоткрытого диапазона candles может получить соответственно этот
интервал или диапазон. Это не manual sync; повторное чтение не расширяет и не
запускает такую работу заново.
- Portfolio, operations, forecasts и события сохраняют собственные complete
результаты и время последнего успешного чтения. Результат ресурса
(freshness, completeness, `lastSuccessfulAt`, availability) независим от
resource-relevant последней часовой попытки: поздняя partial/failed попытка
не заменяет сохранённый complete result. При его отсутствии `data: null`.
- Detail, company, catalog search и ticker lookup получают ответ онлайн через - Detail, company, catalog search и ticker lookup получают ответ онлайн через
существующие integration clients. Их ошибки не заменяют сохранённые существующие integration clients. Их ошибки не заменяют сохранённые
финансовые данные или подписи snapshot. финансовые данные или подписи snapshot.
- Старые справочные строки не являются источником актуальности и удаляются - Старые справочные строки не являются источником актуальности и удаляются
только отдельной совместимой миграцией после проверки FK и свечей. только отдельной совместимой миграцией после проверки FK и свечей.
- Клиент повторяет открытые инвестиционные GET не чаще одного раза в 30 минут; - Клиент повторяет открытые инвестиционные GET не чаще одного раза в 30 минут;
retry повторяет чтение и не создаёт screen sync work. retry повторяет чтение и не создаёт screen sync work. Наблюдение последней
попытки, включая Accounts `sync`, не является командой.
## Consequences ## Consequences
Карточки инструментов зависят от доступности публичного upstream в момент Карточки инструментов зависят от доступности публичного upstream в момент
чтения. Финансовые экраны сохраняют последний успешный snapshot, а статус чтения. Финансовые экраны сохраняют последний успешный snapshot, а статус
последнего hourly walk показывается отдельно. OpenAPI backend остаётся единственным последнего hourly walk показывается отдельно. OpenAPI backend остаётся
источником HTTP-контракта; frontend использует опубликованный snapshot. единственным исполняемым источником HTTP-контракта; frontend использует
опубликованный snapshot.
+47 -37
View File
@@ -1,54 +1,64 @@
# Свежесть данных и повторное использование # Свежесть данных и повторное использование
WealthLedger читает инвестиционные данные из SQLite. Обычный GET возвращает WealthLedger читает личные инвестиционные данные из SQLite. Обычный GET и его
сохранённый результат и не запускает личную синхронизацию. Общие справочные HTTP retry возвращают сохранённый результат и не запускают личную работу с
данные инструментов запрашиваются онлайн, когда это требуется карточке; T-Bank. Нормальным владельцем обновления служит часовой обход текущего
финансовый HTTP-ответ не ждёт T-Bank. Модель принята в подключения: после успешного подключения он начинает первую попытку, затем
[ADR 017](../../adr/017-local-tbank-reads-and-background-sync.md). начинает следующую не ранее чем через 60 минут от начала предыдущей. После
простоя выполняется не более одна актуальная попытка, а перекрывающийся срок
пропускается, не ставится в очередь. Общие справочные данные инструментов
запрашиваются онлайн, когда это требуется карточке; финансовый HTTP-ответ не
ждёт T-Bank. Модель принята в
[ADR 019](../../adr/019-hourly-reads-and-online-instrument-reference.md).
## Клиентский и серверный снимки ## Клиентский и серверный снимки
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный [Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный
ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке
запроса в той же границе доступа. SQLite сохраняет источники независимо от HTTP-перечитывания в той же границе доступа. Это не серверный fallback и не
браузера: membership, последний полный portfolio, исполненные операции, состояние часового обхода. SQLite независимо от браузера хранит membership,
общие сведения об инструментах, котировки, свечи и календари. последний полный portfolio, исполненные операции, прогнозы, общие сведения об
инструментах, котировки, свечи и календари.
[Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются. [Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются.
Успешно проверенный пустой интервал полон; последняя страница журнала сама Успешно проверенный пустой результат полон и доступен; последняя страница
по себе полноту не доказывает. Ошибка сверки сохраняет прежние данные и время. журнала сама по себе полноту не доказывает. Результат каждого личного ресурса
Старый полный портфель не удаляется по истечении TTL. `isStale` показывает в `{ data, meta }` описывают пять независимых значений: freshness,
возраст portfolio source, а не сам факт неудачного обновления. completeness, `lastSuccessfulAt`, `lastAttempt` и availability. Первые,
полнота и `lastSuccessfulAt` относятся только к возвращённому подтверждённому
результату. `lastAttempt` описывает относящуюся к ресурсу часовую попытку:
`none`, `running`, `succeeded`, `partial` или `failed`.
| Источник | Когда обычный запрос требует обновления | Поздняя неуспешная или частичная попытка сохраняет прежний полный результат и
его исходное время. Если полного результата нет, `data` равно `null`: состояние
availability объясняет первоначальную загрузку, временную недоступность либо
недоступность подключения. Пустой массив не означает отсутствие результата.
Старый полный portfolio не удаляется из-за возраста. `Portfolio.isStale`
сохраняет смысл возраста его snapshot, а не обозначает неуспех обхода или
клиентскую ошибку перечитывания.
| Источник | Владелец нормального обновления |
| --- | --- | | --- | --- |
| Portfolio, котировки, изменяемый хвост дневных свечей | Через 60 секунд | | Membership, portfolio, изменяемый хвост операций и события | Часовой обход текущего подключения |
| Membership и изменяемый хвост операций | Через 5 минут | | Котировки и изменяемый хвост дневных свечей | Установленная политика источника; не ordinary personal GET |
| Metadata, поиск каталога и company | Онлайн при чтении | | Metadata, поиск каталога и company | Онлайн при чтении |
| Календари | Через часовой обход | | Полные прошлые интервалы операций и завершённые свечи | Не обновляются часовым обходом или обычным GET |
| Полные прошлые интервалы операций и завершённые свечи | При явном обновлении показанного периода |
TTL не создаёт таймер импорта. Запрошенная работа может закончиться после Два исключения разрешают только первое получение отсутствующего результата:
ухода со страницы. После перезапуска нет автоматического обхода intent: первое открытие неподтверждённого исторического интервала операций получает
следующий подходящий запрос повторяет незавершённый интервал с первой страницы, только этот интервал и возвращает локальные строки с неполным покрытием; первое
переиспользуя полные интервалы. Cursor брокера не служит durable checkpoint. открытие диапазона свечей без подтверждённого локального результата получает
только этот диапазон. Ни одно исключение не обновляет membership, portfolio,
события, другой интервал или диапазон. Их HTTP retry также только перечитывает
локальное состояние. Подтверждённый полный интервал или диапазон остаётся
локальным.
Повторное чтение перечитывает общие справочные данные онлайн, а финансовые Часовой обход сохраняет успешные разделы независимо, а failed section не
ресурсы используют сохранённый результат и применяемую политику свежести. заменяет прежний complete result. Отменой query lifecycle владеет TanStack
Повторная сверка полного периода публикуется одной Query. Открытый экран перечитывает ресурс не чаще одного раза в 30 минут;
транзакцией после всех страниц, интервалов и счетов, включая `all`. это локальный HTTP read. Совместимый параметр `sync=observe`, пока он есть в
Сбой сохраняет весь прежний результат. Первая загрузка может публиковать маршруте, также является только чтением и не представляет команду обходу.
полные малые интервалы и показывать частичный период без окончательных сумм. [HTTP API](../http-api.md) описывает исполняемый контракт envelope.
Клиент наблюдает только известную активную работу открытого экрана.
Status GET и data GET с `sync=observe` не запускают работу. После изменения
состояния ответ попадает в ту же query identity; после ухода или завершения
наблюдение прекращается. Поздний ответ не заменяет уже принятый новый.
[HTTP API](../http-api.md) объясняет envelope и экранный refresh.
Отменой query lifecycle владеет TanStack Query. Status не пересобирает
финансовые ответы и не передаёт hash notifications. Свечи нового диапазона
загружаются при его открытии; предзагрузка всей истории портфеля не выполняется.
## Смена подключения ## Смена подключения
+21 -20
View File
@@ -2,7 +2,8 @@
## Источник контракта ## Источник контракта
Backend генерирует OpenAPI из собственного публичного API. Frontend использует Backend генерирует OpenAPI из собственного публичного API. Этот сгенерированный
OpenAPI — исполняемый источник HTTP-контракта. Frontend использует
предоставленный OpenAPI snapshot как актуальный вход и локально генерирует предоставленный OpenAPI snapshot как актуальный вход и локально генерирует
типизированный клиент. Backend владеет контрактом, а подготовка и доставка типизированный клиент. Backend владеет контрактом, а подготовка и доставка
схемы находятся вне frontend. Это разделение принято в схемы находятся вне frontend. Это разделение принято в
@@ -22,27 +23,27 @@ environment policy, а не используется frontend в runtime.
## Инвестиционные данные и синхронизация ## Инвестиционные данные и синхронизация
T-Bank data GET возвращают envelope `{ data, meta }`. `data` может быть `null` Затронутые GET инвестиционных ресурсов возвращают сгенерированный contract
при missing или неполном финансовом агрегате; partial ledger/candles могут `{ data, meta }`. `data` nullable: `null` означает отсутствие complete result,
содержать подтверждённые строки. `meta` разделяет availability, freshness, а подтверждённый пустой массив является available complete result. `meta`
lastSuccessfulAt, sync, coverage и состояния зависимостей. `dataRevision` независимо описывает freshness, completeness, `lastSuccessfulAt`,
описывает набор данных; пустое завершённое покрытие явно представлено в coverage. resource-relevant `lastAttempt` и availability. `lastAttempt` имеет состояния
Публичной hash-версии состояния нет. Ни одно поле не раскрывает connection revision. `none`, `running`, `succeeded`, `partial` и `failed`; его неуспех не заменяет
сохранённый complete result или его время. `Portfolio.isStale` остаётся
специфичным для snapshot полем, а не заменой состояния попытки.
Обычный GET планирует missing/stale зависимости после проверки доступа. Только Accounts дополнительно возвращает top-level `sync`: `null`, если у
GET с `sync=observe` только читает. `POST /api/v1/tbank-sync` принимает текущего подключения ещё не было часовой попытки, иначе connection-wide
валидированный screen descriptor и возвращает `202` с handle и нормализованным наблюдаемый результат. Это наблюдение, не команда. Обычный GET и retry после
scope. `GET /api/v1/tbank-sync/:id` наблюдает разрешённую работу; чужой или проверки доступа читают локальное состояние и не начинают личную работу.
прежний handle возвращает `404`. Приём работы не означает её успех. Устаревший `sync=observe`, пока он сохранён для совместимости маршрута, также
только читает. Request-started screen-sync command/status не является текущей
AS-IS моделью.
Status возвращает state/errorCode без данных ресурсов и численных progress Безопасные ошибки завершённой часовой попытки передаются в `lastAttempt` и
counters. Клиент перечитывает показанные resources через `sync=observe` во время Accounts `sync`; HTTP transport/access/input errors сохраняют Problem Details.
работы и после завершения, сохраняя generated query identity. Sync states ресурса: Старые данные при временной source failure сохраняют исходное успешное время.
`idle`, `running`, `failed`; отдельного публичного `queued` нет. Ни одно поле не раскрывает connection revision. Детали поведения —
Source failure принятой работы передаётся безопасным `meta.sync.errorCode`;
HTTP transport/access/input errors сохраняют Problem Details. Старые данные
при transient failure сохраняют исходное успешное время. Детали поведения —
[свежесть и полнота](cross-cutting/data-freshness.md). [свежесть и полнота](cross-cutting/data-freshness.md).
Envelope несовместим с прежними bare-array ответами: backend/frontend Envelope несовместим с прежними bare-array ответами: backend/frontend
+30 -23
View File
@@ -40,10 +40,10 @@
## Период и список ## Период и список
Без фильтра период начинается сегодня в UTC и заканчивается через 30 дней Без фильтра период начинается сегодня в UTC и заканчивается через 30 дней
после начала, обе даты включены. Поля «С» и «По» позволяют изменить период: после начала, обе даты включены. `from` и `to` принимают только строгие даты
«Применить» сохраняет выбранные календарные даты в URL и запрашивает результат, `YYYY-MM-DD`; без `from` используется текущая дата UTC, без `to` — `from + 30`
«Сбросить» возвращает период по умолчанию. Обзор счёта сохраняет прогноз календарных дней. Поля «С» и «По» сохраняют выбранные календарные даты в URL,
на текущую дату и следующие 30 дней. а «Сбросить» возвращает период по умолчанию.
События упорядочены по календарной дате. У каждой строки видны тип, События упорядочены по календарной дате. У каждой строки видны тип,
инструмент, дата и доступная сумма в исходной валюте. Список не имеет инструмент, дата и доступная сумма в исходной валюте. Список не имеет
@@ -52,10 +52,13 @@
отсутствия всех будущих выплат. Инструмент без поддерживаемых данных отсутствия всех будущих выплат. Инструмент без поддерживаемых данных
не получает искусственную строку «нет данных». не получает искусственную строку «нет данных».
API принимает необязательные `from` и `to` в формате `YYYY-MM-DD` для Сохранённый forecast сохраняет свой `calculatedAt` и исходное включительное
отдельного и общего счёта. Без `from` используется сегодня в UTC, окно. GET для отдельного счёта или `all` возвращает только пересечение этого
без `to` — 30 дней после начала. Неверный формат или несуществующая дата окна и запрошенного UTC-периода, не меняя ни `calculatedAt`, ни исходное окно.
дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`. Пустое пересечение или отсутствие событий в пересечении — успешный пустой
список с теми же metadata; это не отсутствие forecast. Неверный формат или
несуществующая дата дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`
без замены периода и без запуска личной работы.
Фильтрация на сервере предшествует сортировке и вычислению Фильтрация на сервере предшествует сортировке и вычислению
`incomeTotalsByCurrency`, включающего только купоны и дивиденды. `incomeTotalsByCurrency`, включающего только купоны и дивиденды.
Итоги остаются в ответе API; полный экран показывает список без общего итога. Итоги остаются в ответе API; полный экран показывает список без общего итога.
@@ -63,26 +66,30 @@ API принимает необязательные `from` и `to` в форма
## Получение и отказы ## Получение и отказы
Запрос требует сессии, подключения и локально подтверждённого membership. Запрос требует сессии, подключения и локально подтверждённого membership.
Прогноз строится из личного сохранённого portfolio и общих календарей: T-Bank для Он читает сохранённый forecast и не запускает личную синхронизацию. Часовой
дивидендов, MOEX для bond events. Отдельного синхронного получения portfolio обход строит forecast из полного личного portfolio и общих календарей: T-Bank
нет. Личные оценки сохраняются отдельно от общих календарей. для дивидендов, MOEX для bond events. Личные оценки сохраняются отдельно от
общих календарей.
Последний полный forecast привязан к scope, периоду и membership. Последний полный forecast привязан к scope, исходному окну и membership. Если
Если новый portfolio требует ещё недоступного календаря, прежний полный часовой обход не может получить полный новый forecast, прежний полный прогноз,
прогноз и его source times остаются, в том числе после рестарта. Новый его `calculatedAt` и исходное окно остаются читаемыми, в том числе после
полный результат заменяет его атомарно. Без fallback показывается рестарта; `meta.lastAttempt` отдельно сообщает об ошибке обновления. Без
missing/partial, а не подтверждённое отсутствие выплат. Другой период или полного forecast `data: null` и metadata обозначает initial loading либо
изменившийся membership не получают старый прогноз под новым названием. временную недоступность, а не подтверждённое отсутствие выплат. В режиме `all`
Source failure сообщается в metadata и не изменяет успешное время. результат complete только при полном вкладе каждого включённого счёта. Другой
scope или изменившийся membership не получают старый forecast под новым
названием.
Успешный ответ MOEX без запрошенного блока означает отсутствие поддерживаемых Успешный ответ MOEX без запрошенного блока означает отсутствие поддерживаемых
событий этого источника; это отличается от transport failure и повреждённого событий этого источника; это отличается от transport failure и повреждённого
формата ответа. формата ответа.
При первом открытии показан каркас заголовка и строк, без интерактивных При первом открытии без complete forecast показан каркас заголовка и строк,
заглушек. Пустой ответ сохраняет возможность изменить период. без интерактивных заглушек. Пустой успешный ответ сохраняет возможность
При обычном фоновом обновлении уже полученные данные остаются на экране, изменить период. При failed HTTP reread уже полученный Client Cached Snapshot
ошибка получает предупреждение и повторную попытку. Без выбранного счёта остаётся на экране; это отличается от server-side failed hourly attempt.
запрос не выполняется. Смена сессии и подключения следует Повторная попытка перечитывает только HTTP-ответ. Без выбранного счёта запрос
не выполняется. Смена сессии и подключения следует
[общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md). [общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md).
Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md) Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md)
+32 -31
View File
@@ -1,46 +1,47 @@
# Загрузка портфеля # Загрузка портфеля
Экран `/accounts/:accountId` читает последний полный snapshot из SQLite Экран `/accounts/:accountId` читает последний полный snapshot из SQLite после
после проверки сессии, подключения и локального membership. Если список проверки сессии, подключения и локального membership. Если список счетов ещё
счетов ещё неизвестен, сервер сообщает о загрузке доступа без выдачи личных неизвестен, сервер сообщает о загрузке доступа без выдачи личных данных
данных неизвестного ID. Известный недоступный счёт возвращает `404`. неизвестного ID. Известный недоступный счёт возвращает `404`.
## Основной обмен ## Основной обмен
Обычный GET возвращает локальный envelope. Missing/stale зависимости Обычный GET и retry возвращают read-only локальный `{ data, meta }` envelope:
запрашиваются в фоне; чтение не ждёт T-Bank. Для нового полного snapshot они не ждут и не вызывают T-Bank и не предлагают ручную синхронизацию. Часовой
нужны оба финансовых ответа `GetPortfolio` и `GetPositions`. Суммы и позиции обход текущего подключения обновляет Portfolio независимо от экрана. Для нового
публикуются вместе; metadata дополняет представление независимо, с доступным полного snapshot ему нужны оба финансовых ответа `GetPortfolio` и
UID/FIGI вместо имени до её загрузки. `GetPositions`; суммы и позиции публикуются вместе. Metadata инструмента
дополняет представление независимо, с доступным UID/FIGI вместо имени до её
онлайн-загрузки.
Свежесть portfolio — 60 секунд. Истечение срока не удаляет snapshot и не Открытый экран выполняет локальное перечитывание не чаще одного раза в
создаёт таймер. «Обновить» на обзоре перечитывает зависимости текущего экрана: 30 минут; retry также повторяет только HTTP GET. `data: null` означает, что
membership, portfolio, показанные операции, доход и будущие события. полного snapshot ещё нет, а не нулевой портфель. Пустой подтверждённый portfolio
Секции могут быть готовы независимо. Подробнее — остаётся available complete result. Секции могут быть готовы независимо.
[свежесть](../../../architecture/cross-cutting/data-freshness.md). Подробнее — [свежесть](../../../architecture/cross-cutting/data-freshness.md).
## Границы доступа ## Границы доступа
Публикация проверяет connection revision и attempt в той же транзакции. Публикация проверяет connection revision и attempt в той же транзакции. Замена
Замена или удаление подключения не допускает позднюю запись прежней работы. или удаление подключения не допускает позднюю запись прежней работы. Frontend
Frontend очищает недоступный personal view и отклоняет запоздалые ответы. очищает недоступный personal view и отклоняет запоздалые ответы. В режиме
В режиме [«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав [«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав accounts и
accounts и snapshots читается согласованно, но их source times могут различаться. snapshots читается согласованно, но их source times могут различаться.
## Ошибки и результат для пользователя ## Ошибки и результат для пользователя
Нет подключения — `409 TBANK_NOT_CONNECTED`, известный отказ токена — Нет подключения — `409 TBANK_NOT_CONNECTED`, известный отказ токена —
`400 INVALID_TBANK_TOKEN`, недоступный счёт — `404`. Они не маскируются `400 INVALID_TBANK_TOKEN`, недоступный счёт — `404`. Они не маскируются
запасным snapshot. Transient source failure оставляет прежний полный портфель запасным snapshot. Временная ошибка часового обхода оставляет прежний полный
и его исходное время с безопасным предупреждением. При отсутствии snapshot portfolio и его исходное `lastSuccessfulAt`; `meta.lastAttempt` сообщает о
показывается missing/failed, без нулевых итогов. `503` означает ошибку failed или partial попытке отдельно. `Portfolio.isStale` сообщает возраст
обслуживания HTTP-запроса, а не принятой фоновой работы. snapshot, а Client Cached Snapshot и ошибка браузерного HTTP-перечитывания
имеют независимый смысл. При отсутствии полного snapshot `data: null` и
metadata обозначает initial loading либо временную недоступность, без нулевых
итогов. `503` означает ошибку обслуживания самого HTTP-запроса, а не
завершившуюся фоновую работу.
Observer запрашивает статус известной работы, затем `sync=observe` читает Денежные правила находятся в [справочнике](../data-reference.md), проверка
изменившийся ресурс без запуска синхронизации. При уходе и terminal outcome расхождения — в [диагностике](../how-to/investigate-discrepancy.md), RPC/лимиты
наблюдение прекращается. Повторное открытие может запросить недостающее; — в [интеграции T-Bank](../../../integrations/tbank.md).
после рестарта сохранённый snapshot доступен сразу.
Денежные правила находятся в [справочнике](../data-reference.md),
проверка расхождения — в [диагностике](../how-to/investigate-discrepancy.md),
RPC/лимиты — в [интеграции T-Bank](../../../integrations/tbank.md).