Files

59 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
См. proposal.md — Why/What. Текущее состояние: `PositionSection` в `AccountPage.view.tsx:269` рендерит полноширинные `Card`-строки на колонке 960px; позиции уже делятся на секции Акции/Облигации/Другие в upstream-порядке. Backend уже тянет metadata инструмента для каждой позиции (name/ticker) с 24-часовым кэшем; T-Bank в ответе `GetInstrumentBy` дополнительно отдаёт `isin`, но backend его не пробрасывает (`TbankInstrument` типизирован как `{ ticker?, name? }`, `tbank.client.ts:47`).
Важный факт: `totalsByCurrency` включает **и** Cash Balance (`portfolio.service.ts:148` — `new Map(cash)`), поэтому он непригоден как знаменатель доли позиции.
## Goals / Non-Goals
**Goals:**
- Backend отдаёт `isin` позиции без новых обращений к T-Bank.
- Frontend отображает позиции компактными плитками в grid с метриками: идентификатор, количество, `currentValue` (главная), `absoluteReturn` (цвет знака), `averagePurchasePrice`, доля среди позиций той же валюты, `isin`. Null-метрики скрываются.
- Сохранить секции, upstream-порядок, некликабельность, пустые/ошибки/stale-состояния и ровно один способ расчёта доли.
**Non-Goals:**
- Относительная доходность в %, тикер, сортировка, кликабельные карточки, изменение Totals/Cash/«Последних событий», валютная конвертация.
## Decisions
### 1. ISIN прокидывается через существующий metadata-конвейер
`TbankInstrument` расширяется полем `isin?: string`; `position()` в `portfolio.service.ts` возвращает `isin: metadata?.isin?.trim() || null`; `PortfolioPositionDto` получает nullable `isin`. Новых RPC нет — metadata уже запрашивается и кэшируется, меняется только форма значения.
*Альтернатива:* отдельный endpoint инструментов — overkill; маппинг на фронте — невозможно (ISIN нет в данных).
### 2. Доля позиции считается на клиенте, знаменатель — только позиции одной валюты
Знаменатель = Σ `currentValue` **позиций** одной валюты, без Cash Balance. Сумма вычисляется во `view` (или VM) по всему массиву `positions` один раз (Map валюта → сумма) и передаётся в плитки. Важно: доля считается **по всем секциям сразу**, а не внутри секции — иначе USD-облигация и USD-акция считались бы раздельно.
*Альтернатива:* знаменатель из `totalsByCurrency` — отклонено: он включает кэш и занижает долю. Backend-расчёт — отклонено: доля это отображаемая метрика, спека закрепляет её за клиентом.
### 3. Расчёт доли — display-only, точность Number с округлением
`display.ts` получает helper `formatShare(numerator, denominator)` → строка процента (1 знак) или `null` при недоступности. Значения money — decimal-строки; для отображения доли достаточно `Number(value)` и округления: доля не участвует в финансовой арифметике и не передаётся в агрегаты. Один маленький `assert`-тест в `display.test.ts`.
*Risk/mitigation:* значения крупнее 2^53 теряют точность — неприменимо к реальным суммам портфеля и безвредно для display-only.
### 4. Grid — Mantine `SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}`
Тот же компонент, что уже используется для Totals-карточек. Плитки заменяют `Card`-строки в `PositionSection`; на узком экране — одна колонка (спека «Портфель отображается на мобильном устройстве» остаётся верной).
### 5. Структура плитки
- Верх: `displayName` (усечение), `isin` (dim, `mono`, xs) под именем, количество (dim, sm).
- Справа/ниже: `currentValue` крупно (главная метрика), `absoluteReturn` с цветом знака.
- Низ: `averagePurchasePrice` и доля (dim, xs).
### 6. Цвета — Mantine-переменные
Зелёный `var(--mantine-color-green-6)` / красный `var(--mantine-color-red-6)` для `absoluteReturn`; без цвета при `null`. Кастомную палитру не вводим.
### 7. Null-метрики скрываются
Вместо «—» строка не рендерится (компактная плитка). Это меняет текущий тест `AccountPage.test.tsx:94` (`expect(screen.getByText("—"))`).
## Risks / Trade-offs
- [Точность доли при Number] → доля display-only, округление до 1 знака; upgrade до BigInt-деления, если появится сценарий, требующий точности.
- [ISIN отсутствует у валют и части инструментов] → nullable; плитка скрывает строку ISIN.
- [Grid-плитки на узком экране] → `base: 1` гарантирует одноколоночность.
- [«—»-заглушка в тестах] → обновить `AccountPage.test.tsx` на проверку скрытия метрик.
## Migration Plan
Обратимая аддитивная миграция. Backend-поле `isin` nullable: старый frontend игнорирует лишнее поле; старый backend без `isin` даёт `undefined` → плитка скрывает строку. Откат — revert соответствующего коммита в репозитории. Снапшот OpenAPI в frontend обновляется `npm run openapi:sync` после изменения backend.
## Open Questions
Нет. Детали типографики и отступов плитки решаются на этапе реализации без изменения спек.