spec: add stock screener
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# System Analysis
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
| Требование delta spec | Межсистемная граница и поток |
|
||||
| --- | --- |
|
||||
| Скринер ищет среди доступных к покупке акций | Запуск поиска, полный список Shares, обязательные признаки и время получения: [поиск](sequences/stock-screener.mmd), [контракт](contracts/stock-screener.openapi.yaml). |
|
||||
| Условия скринера применяются совместно | Необязательные параметры в запросе, отбор по всему полученному списку до разделения на страницы; неизвестное не совпадает. |
|
||||
| Результат представляет отдельные выпуски | После отбора группировка по нормализованному ISIN, иначе по UID; карточка выбранного представителя с точным UID. |
|
||||
| Страницы относятся к одному результату поиска | Создание нового результата при каждом запуске, чтение последующих страниц того же результата по непрозрачному идентификатору; не более 30 выпусков на страницу. |
|
||||
| Отсутствие совпадений отличается от недоступности поиска | Успешный пустой набор имеет `200` и `items: []`; ошибка каталога или потеря результата возвращает ошибку, не пустую страницу. |
|
||||
|
||||
## Systems and Responsibilities
|
||||
|
||||
- Пользователь задаёт условия и запускает поиск в приложении.
|
||||
- Frontend хранит черновик условий отдельно от применённого поиска, показывает карточки, время получения и управляет навигацией по страницам. Он не отбирает только первые найденные строки и не склеивает результаты разных поисков.
|
||||
- Backend проверяет локальную сессию и действующее подключение T-Bank, получает полный список акций, применяет обязательные признаки и выбранные условия, выбирает один подходящий инструмент на выпуск, фиксирует результат и выдаёт страницы. Инструмент и личный портфель — разные данные.
|
||||
- T-Invest поставляет список акций с характеристиками и признаками доступности. Это источник сведений для нового поиска, но не подтверждение доступа конкретного счёта к сделке.
|
||||
|
||||
## Interaction Flows
|
||||
|
||||
[stock-screener.mmd](sequences/stock-screener.mmd) описывает новый поиск без текста, переход к следующей странице того же результата, повторный поиск, неподтверждённую доступность инструмента, отказ внешнего источника и отсутствие сохранённого результата. Чтение карточки выбранной акции после перехода по ссылке относится к существующему маршруту инструмента.
|
||||
|
||||
## Contracts
|
||||
|
||||
- **Frontend → Backend**, HTTP JSON, новая пара операций `POST /api/v1/stocks/screener/search` и `GET /api/v1/stocks/screener/search/{searchId}`. Целевой контракт версии `/api/v1`: [stock-screener.openapi.yaml](contracts/stock-screener.openapi.yaml). Каждый POST — новый поиск даже при совпадающих параметрах; повтор того же POST не идемпотентен в смысле получения данных. GET следующей страницы не повторяет обращение к каталогу. Ответ содержит `items`, `asOf`, `searchId`, `page`, `hasNext`. `searchId` непрозрачен и принадлежит аутентифицированному пользователю и текущему подключению; `page` — номер страницы с единицы. Контракт не задаёт срок хранения результата и не позволяет подменить применённые условия в запросе страницы.
|
||||
- **Backend → T-Invest**, существующий gRPC over TLS: `InstrumentsService.Shares(InstrumentsRequest)` с `instrument_status = INSTRUMENT_STATUS_ALL` возвращает список `Share`. Базовый список не должен заранее исключать выпуск, у которого оба необходимых признака подтверждены. Отбор по сектору, типу, валюте, площадке, стране риска и лоту выполняется после получения всего списка: этих параметров нет в `InstrumentsRequest`. Ожидаются `buy_available_flag`, `api_trade_available_flag`, `uid`, `isin`, `ticker`, `name`, `sector`, `share_type`, `currency`, `real_exchange`, `country_of_risk`, `lot`. `FindInstrument` с обязательной поисковой строкой не является источником полного скринера.
|
||||
- Контракт старого `GET /api/v1/stocks?query=...` не меняется в этой версии. Его обязательный текст, ограничение и текущая форма ответа продолжают работать для существующих потребителей; новое представление не выводится из его первых 30 результатов.
|
||||
|
||||
## Data and State Semantics
|
||||
|
||||
Источник истины для нового поиска — полученный целиком ответ Shares, а не накопленные инструменты портфеля или прежние текстовые запросы. Backend фиксирует полный набор совпавших выпусков и время успешного получения каталога до выдачи первой страницы. Пустая выдача подтверждается только успешным получением списка. Результат поиска отделён от долговременного самостоятельного справочника инструментов; правила существующего `online-instrument-information` остаются в силе.
|
||||
|
||||
Признаки `buy_available_flag = true` и `api_trade_available_flag = true` обязательны вместе. Неизвестное либо неподтверждённое значение любого из них исключает инструмент. Эти признаки относятся к снимку каталога, а не к биржевой сессии, счёту пользователя или исполнению заявки.
|
||||
|
||||
Применённые условия сравниваются с данными конкретного инструмента: текст — с названием или тикером, выбранные категории — с соответствующими значениями, `maxLot` — с известным положительным размером лота не больше границы. Отсутствие выбранного свойства исключает инструмент; отсутствие незаданного свойства не мешает совпадению. Страна риска — код страны основного бизнеса, а не юрисдикция регистрации. «Площадка» означает `real_exchange`, а не секцию/режим `class_code` и не дилерский `instrument_exchange`.
|
||||
|
||||
После отбора инструменты с одинаковым нормализованным ISIN представляют один выпуск; пустой ISIN заменяется точным UID, а не общим пустым ключом. При нескольких подходящих инструментах одного выпуска выбор представителя должен быть детерминированным; он определяет ссылку и все сведения карточки, данные режимов не смешиваются. Затем выпуски упорядочиваются по тикеру и UID выбранного инструмента и делятся на страницы по 30. `asOf` и порядок неизменны для всех страниц одного `searchId`. Повторное нажатие «Найти» не использует старый результат как новый, даже если условия совпадают.
|
||||
|
||||
## Error and Recovery Semantics
|
||||
|
||||
- Неверные границы или формат условий / страницы: `400` с публичной ошибкой в стандартном профиле. Отсутствующая сессия: `401`. Отсутствующее/недействительное подключение T-Bank: соответствующая ошибка доступа подключения; секрет и upstream-исключение не раскрываются.
|
||||
- Источник каталога недоступен, возвращает неполный или неинтерпретируемый список: ошибка запроса, не `items: []`. При новом поиске предыдущий успешно показанный результат не становится доказательством нового поиска. Пользователь может повторить запуск.
|
||||
- Идентификатор результата больше не доступен или не принадлежит текущему пользователю/подключению: ошибка вместо пустой страницы или пересчёта по новому каталогу. Frontend предлагает выполнить новый поиск. Потеря результата в рамках действующего поиска — техническая деградация; время и данные прошлого результата не выдаются за новый.
|
||||
- Нет утверждений о «прямо сейчас можно купить»: статус торгов и права конкретного счёта не проверяются. Недоступная цена/логотип не меняет состав подходящих выпусков.
|
||||
|
||||
## Observability Semantics
|
||||
|
||||
Существенны попытка нового поиска, успешное получение каталога (`asOf`, число исходных инструментов, подходящих выпусков), технический отказ источника, чтение страницы недоступного результата и отказ доступа к чужому результату. Секрет подключения, пользовательский токен и полный текст запроса не нужны для диагностики. Успешный пустой результат отличим от отказа источника.
|
||||
|
||||
## Security and NFR at System Boundaries
|
||||
|
||||
Обе HTTP-операции требуют локальной аутентификации; доступ к T-Invest выполняется только на backend с токеном текущего подключения. Идентификатор результата не является правом доступа и не переносит данные между пользователями или поколениями подключения. Полный каталог нужен до пагинации, чтобы 30 карточек не превратились в ограничение области поиска. Значение `asOf` относится к получению каталога, не к актуальности возможности купить позже. Количественная задержка/SLA для получения списка пока не установлена; не следует обещать мгновенную выдачу при сбое внешнего сервиса.
|
||||
|
||||
## Compatibility / Migration
|
||||
|
||||
Новый маршрут добавляется без изменения действующего текстового поиска и адресов отдельных акций. Frontend переходит на новый контракт для скринера; старый маршрут остаётся доступен его текущим потребителям. После реализации backend-generated OpenAPI должен быть семантически совместим с целевым контрактом в архивируемом change. Документ в этом change не становится вторым постоянным источником исполняемой схемы.
|
||||
|
||||
## Durable Documentation Candidates
|
||||
|
||||
- Новое назначение раздела акций, набор фильтров и смысл «доступна к покупке по данным каталога» в описании домена акций.
|
||||
- Граница между online-каталогом, страницей акции и личным портфелем; снимок результата, время получения и стабильные страницы.
|
||||
- Семантика новых HTTP-операций на уровне описания API без дублирования machine-readable OpenAPI. Окончательный выбор принадлежит `docs-impact`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет блокирующих вопросов по согласованным границам поведения. Выбор механизма хранения результата и допустимой внутренней длительности его доступности принадлежит Engineering Design; потеря результата остаётся ошибкой, а не молчаливой заменой поиска.
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: WealthLedger stock screener target contract
|
||||
version: 1.0.0
|
||||
description: Целевой контракт change; исполняемый HTTP-контракт после реализации генерирует backend.
|
||||
paths:
|
||||
/api/v1/stocks/screener/search:
|
||||
post:
|
||||
operationId: createStockScreenerSearch
|
||||
summary: Получить новый результат скринера по полному каталогу акций
|
||||
security:
|
||||
- bearer: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/StockScreenerCriteria'
|
||||
responses:
|
||||
'200':
|
||||
description: Первая страница нового результата, включая подтверждённый пустой список
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/StockScreenerPage'
|
||||
'400':
|
||||
$ref: '#/components/responses/Problem'
|
||||
'401':
|
||||
$ref: '#/components/responses/Problem'
|
||||
'503':
|
||||
$ref: '#/components/responses/Problem'
|
||||
/api/v1/stocks/screener/search/{searchId}:
|
||||
get:
|
||||
operationId: readStockScreenerPage
|
||||
summary: Прочитать страницу того же зафиксированного результата
|
||||
security:
|
||||
- bearer: []
|
||||
parameters:
|
||||
- name: searchId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Непрозрачный идентификатор результата; не является полномочием доступа.
|
||||
- name: page
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Номер страницы с единицы. Условия поиска при чтении страницы не меняются.
|
||||
responses:
|
||||
'200':
|
||||
description: Страница зафиксированного результата с тем же временем получения каталога
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/StockScreenerPage'
|
||||
'400':
|
||||
$ref: '#/components/responses/Problem'
|
||||
'401':
|
||||
$ref: '#/components/responses/Problem'
|
||||
'404':
|
||||
$ref: '#/components/responses/Problem'
|
||||
'503':
|
||||
$ref: '#/components/responses/Problem'
|
||||
components:
|
||||
securitySchemes:
|
||||
bearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
responses:
|
||||
Problem:
|
||||
description: Ошибка в текущем публичном профиле WealthLedger; отсутствие результата не кодируется пустым списком.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
schemas:
|
||||
Problem:
|
||||
type: object
|
||||
required: [status, code]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
detail:
|
||||
type: string
|
||||
StockScreenerCriteria:
|
||||
type: object
|
||||
description: Все условия необязательны; отсутствие полей означает поиск по всем доступным к покупке акциям каталога.
|
||||
additionalProperties: false
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
maxLength: 80
|
||||
description: Необязательная подстрока названия или тикера; пустая строка равнозначна отсутствию условия.
|
||||
sector:
|
||||
type: string
|
||||
minLength: 1
|
||||
shareType:
|
||||
type: string
|
||||
minLength: 1
|
||||
currency:
|
||||
type: string
|
||||
minLength: 1
|
||||
realExchange:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Реальная площадка исполнения, не код торгового режима.
|
||||
countryOfRisk:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Код страны основного бизнеса.
|
||||
maxLot:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Включительная верхняя граница числа бумаг в лоте.
|
||||
StockScreenerPage:
|
||||
type: object
|
||||
required: [searchId, asOf, page, hasNext, items]
|
||||
properties:
|
||||
searchId:
|
||||
type: string
|
||||
description: Идентификатор только этого поиска, ограничен пользователем и подключением.
|
||||
asOf:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Время получения полного списка акций от T-Invest, одинаковое на всех страницах результата.
|
||||
page:
|
||||
type: integer
|
||||
minimum: 1
|
||||
hasNext:
|
||||
type: boolean
|
||||
items:
|
||||
type: array
|
||||
maxItems: 30
|
||||
items:
|
||||
$ref: '#/components/schemas/StockScreenerItem'
|
||||
StockScreenerItem:
|
||||
type: object
|
||||
required: [instrumentUid, name, ticker, isin, shareType, sector, currency, realExchange, countryOfRisk, lot]
|
||||
properties:
|
||||
instrumentUid:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Точный выбранный инструмент для ссылки на страницу акции; нельзя подменять неоднозначным тикером.
|
||||
name:
|
||||
type: string
|
||||
ticker:
|
||||
type: string
|
||||
nullable: true
|
||||
isin:
|
||||
type: string
|
||||
nullable: true
|
||||
shareType:
|
||||
type: string
|
||||
nullable: true
|
||||
sector:
|
||||
type: string
|
||||
nullable: true
|
||||
currency:
|
||||
type: string
|
||||
nullable: true
|
||||
realExchange:
|
||||
type: string
|
||||
nullable: true
|
||||
countryOfRisk:
|
||||
type: string
|
||||
nullable: true
|
||||
lot:
|
||||
type: integer
|
||||
minimum: 1
|
||||
nullable: true
|
||||
@@ -0,0 +1,40 @@
|
||||
sequenceDiagram
|
||||
actor U as Пользователь
|
||||
participant F as WealthLedger frontend
|
||||
participant B as WealthLedger backend
|
||||
participant T as T-Invest API
|
||||
U->>F: Выбирает условия и нажимает «Найти»
|
||||
F->>B: POST /api/v1/stocks/screener/search (условия)
|
||||
B->>B: Проверка сессии и подключения
|
||||
alt Нет доступа или неверные условия
|
||||
B-->>F: 400 / 401 / ошибка подключения
|
||||
F-->>U: Объяснение ошибки без нового результата
|
||||
else Доступ подтверждён
|
||||
B->>T: Shares (INSTRUMENT_STATUS_ALL)
|
||||
alt Каталог получен полностью
|
||||
T-->>B: Share[]
|
||||
B->>B: Проверить доступность, фильтры, выпуски, сортировку и зафиксировать asOf
|
||||
B-->>F: 200, items (до 30), searchId, asOf, hasNext
|
||||
F-->>U: Первая страница или подтверждённый пустой результат
|
||||
opt Есть следующая страница
|
||||
U->>F: Далее
|
||||
F->>B: GET /api/v1/stocks/screener/search/{searchId}?page=2
|
||||
alt Результат доступен тому же пользователю и подключению
|
||||
B-->>F: 200, следующие items, тот же asOf
|
||||
F-->>U: Следующая страница без пропусков и повторов
|
||||
else Результат недоступен или сменилось подключение
|
||||
B-->>F: Ошибка без пересчёта по новому каталогу
|
||||
F-->>U: Результат недоступен, предложить новый поиск
|
||||
end
|
||||
end
|
||||
else Каталог недоступен или неполон
|
||||
T-->>B: Ошибка / неполные сведения
|
||||
B-->>F: Ошибка поиска, не пустая выборка
|
||||
F-->>U: Поиск недоступен, можно повторить
|
||||
end
|
||||
end
|
||||
opt Условия изменены или нажат повторный поиск
|
||||
U->>F: Нажимает «Найти»
|
||||
F->>B: Новый POST с текущими условиями
|
||||
Note over B,T: Новый список и новая первая страница без подмены прежнего searchId
|
||||
end
|
||||
Reference in New Issue
Block a user