feat: add bond pages and portfolio navigation

This commit is contained in:
2026-09-10 22:30:47 +03:00
parent 561222e468
commit 232d7f9990
39 changed files with 4217 additions and 221 deletions
+817
View File
@@ -212,6 +212,134 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/bonds/{uid}/metrics": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_metrics"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_search"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds/by-ticker/{ticker}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_getByTicker"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds/{uid}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds/{uid}/quote": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_quote"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds/{uid}/candles": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_candles"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds/{uid}/position": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_position"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/bonds/{uid}/coupons": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["BondController_coupons"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/tbank-connection": {
parameters: {
query?: never;
@@ -582,6 +710,165 @@ export interface components {
estimateUnavailable: boolean;
positionIsStale: boolean;
};
BondCreditRatingDto: {
/** @enum {string} */
agency: "NCR";
value: string | null;
/** @enum {string} */
status: "active" | "withdrawn";
/** Format: date */
date: string;
/** Format: uri */
sourceUrl: string;
/** Format: date-time */
observedAt: string;
};
BondYieldDto: {
/** @description MOEX effective annual yield in percent. */
value: string;
pricePercent: string;
/** Format: date */
date: string;
/** @description MOEX yield target, e.g. MATDATE or OFFERDATE. */
dateType: string | null;
/** Format: date-time */
tradeAt: string;
};
BondMetricsDto: {
creditRating: components["schemas"]["BondCreditRatingDto"] | null;
coupon: components["schemas"]["MoneyDto"] | null;
/** Format: date */
couponDate: string | null;
couponType: string | null;
couponPeriodDays: number | null;
/** @description Next coupon × 365 / period days / indicative clean price × 100. Not yield to maturity. */
couponYieldPercent: string | null;
/** Format: date-time */
couponYieldPriceAt: string | null;
effectiveYield: components["schemas"]["BondYieldDto"] | null;
unavailable: string[];
/** Format: date-time */
observedAt: string;
};
BondSearchResultDto: {
/** Format: uuid */
instrumentUid: string;
/** @description Unambiguous ticker URL segment, or the exact instrument UID. */
slug: string;
name: string;
isin: string | null;
classCode: string | null;
realExchange: string | null;
apiTradeAvailable: boolean | null;
ticker: string | null;
logoName: string | null;
currency: string | null;
sector: string | null;
lot: number | null;
/** Format: date-time */
historyFrom: string | null;
nominal: components["schemas"]["MoneyDto"] | null;
initialNominal: components["schemas"]["MoneyDto"] | null;
aci: components["schemas"]["MoneyDto"] | null;
maturityDate: string | null;
callDate: string | null;
bondType: string | null;
riskLevel: string | null;
couponQuantityPerYear: number | null;
floatingCoupon: boolean | null;
perpetual: boolean | null;
amortization: boolean | null;
subordinated: boolean | null;
/** @description Clean price in percent of nominal, not money. */
pricePercent: string | null;
/** Format: date-time */
priceAt: string | null;
};
BondDto: {
/** Format: uuid */
instrumentUid: string;
/** @description Unambiguous ticker URL segment, or the exact instrument UID. */
slug: string;
name: string;
isin: string | null;
classCode: string | null;
realExchange: string | null;
apiTradeAvailable: boolean | null;
ticker: string | null;
logoName: string | null;
currency: string | null;
sector: string | null;
lot: number | null;
/** Format: date-time */
historyFrom: string | null;
nominal: components["schemas"]["MoneyDto"] | null;
initialNominal: components["schemas"]["MoneyDto"] | null;
aci: components["schemas"]["MoneyDto"] | null;
maturityDate: string | null;
callDate: string | null;
bondType: string | null;
riskLevel: string | null;
couponQuantityPerYear: number | null;
floatingCoupon: boolean | null;
perpetual: boolean | null;
amortization: boolean | null;
subordinated: boolean | null;
};
BondQuoteDto: {
pricePercent: string | null;
/** @description Indicative clean value at the current nominal; no FX conversion. */
cleanPrice: components["schemas"]["MoneyDto"] | null;
aci: components["schemas"]["MoneyDto"] | null;
/** @description Indicative clean value plus current accrued interest, excluding fees. */
dirtyPrice: components["schemas"]["MoneyDto"] | null;
changePercentagePoints: string | null;
previousCloseDate: string | null;
tradingStatus: string | null;
/** Format: date-time */
priceAt: string | null;
unavailable: string[];
/** Format: date-time */
observedAt: string;
};
BondCandlesDto: {
candles: components["schemas"]["StockCandleDto"][];
currency: string | null;
/** Format: date-time */
historyFrom: string | null;
/** Format: date-time */
observedAt: string;
};
BondPositionDto: {
quantity: string;
currentValue: components["schemas"]["MoneyDto"] | null;
averagePurchasePrice: components["schemas"]["MoneyDto"] | null;
absoluteReturn: components["schemas"]["MoneyDto"] | null;
returnPercent: string | null;
accounts: components["schemas"]["StockPositionRowDto"][];
isStale: boolean;
/** Format: date-time */
observedAt: string;
};
BondCouponDto: {
number: string | null;
paymentDate: string | null;
recordDate: string | null;
couponType: string | null;
periodDays: number | null;
perBond: components["schemas"]["MoneyDto"] | null;
estimatedAmount: components["schemas"]["MoneyDto"] | null;
/** @enum {string} */
timing: "upcoming" | "past" | "undated";
};
BondCouponsDto: {
coupons: components["schemas"]["BondCouponDto"][];
from: string;
to: string;
observedAt: string;
positionAt: string | null;
estimateUnavailable: boolean;
positionIsStale: boolean;
};
TbankConnectionStatusDto: {
connected: boolean;
/** Format: date-time */
@@ -1651,6 +1938,536 @@ export interface operations {
};
};
};
BondController_metrics: {
parameters: {
query?: never;
header?: never;
path: {
uid: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondMetricsDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_search: {
parameters: {
query: {
query: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondSearchResultDto"][];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_getByTicker: {
parameters: {
query?: never;
header?: never;
path: {
ticker: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_get: {
parameters: {
query?: never;
header?: never;
path: {
uid: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_quote: {
parameters: {
query?: never;
header?: never;
path: {
uid: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondQuoteDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_candles: {
parameters: {
query?: {
period?: "month" | "halfYear" | "year" | "all";
};
header?: never;
path: {
uid: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondCandlesDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_position: {
parameters: {
query?: never;
header?: never;
path: {
uid: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondPositionDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
BondController_coupons: {
parameters: {
query?: never;
header?: never;
path: {
uid: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BondCouponsDto"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
/** @description Upstream service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
};
};
TbankConnectionController_status: {
parameters: {
query?: never;
File diff suppressed because it is too large Load Diff
+104 -1
View File
@@ -1,7 +1,12 @@
const ru = {
translation: {
appName: "WealthLedger",
navigation: { accounts: "Счета", stocks: "Акции", settings: "Настройки" },
navigation: {
accounts: "Счета",
stocks: "Акции",
bonds: "Облигации",
settings: "Настройки",
},
breadcrumbs: {
label: "Хлебные крошки",
accounts: "Брокерские счета",
@@ -11,6 +16,104 @@ const ru = {
events: "Будущие события",
settings: "Настройки",
},
bond: {
instrument: "Облигация",
searching: "Ищем облигации",
noSearchResults:
"Облигации не найдены. Попробуйте другое название, тикер или ISIN.",
searchVariants:
"Одна карточка на выпуск (ISIN). Разные выпуски одного эмитента показаны отдельно.",
chartLabel: "Дневные цены облигации, {{currency}}",
quantity: "Количество облигаций",
historyNote:
"Операции по этой облигации на доступных открытых счетах, включая прошлые годы. Дата сделки — дата операции брокера; количество — исполненная часть. История закрытых счетов не включена.",
couponWindow:
"Календарь выпуска с {{from}} по {{to}}. Личные зачисления показаны в истории событий.",
searchTitle: "Облигации",
searchSubtitle: "Найдите выпуск по названию, тикеру или ISIN.",
searchLabel: "Поиск облигаций",
searchPlaceholder: "Например, ОФЗ или SU26238RMFS4",
loading: "Загрузка облигации",
unavailable: "Облигация недоступна",
unavailableText: "Не удалось найти выпуск или загрузить его данные.",
connect: "Подключите Т-Банк в настройках, чтобы открыть облигации.",
ambiguousTicker:
"Тикер соответствует нескольким выпускам. Выберите облигацию через поиск.",
backToSearch: "К поиску облигаций",
lot: "Размер лота",
unknown: "—",
percentOfNominal: "% от номинала",
chartNote:
"Дневная цена закрытия в процентах от номинала, без НКД. Исторические цены не пересчитаны в деньги по сегодняшнему номиналу.",
terms: "Условия выпуска",
nominal: "Текущий номинал",
initialNominal: "Первоначальный номинал",
maturity: "Погашение",
matures: "Погашение {{date}}",
callDate: "Оферта",
couponFrequency: "Купонов в год",
floatingCoupon: "Плавающий купон",
perpetual: "Бессрочная",
amortization: "С амортизацией",
subordinated: "Субординированная",
aci: "НКД на одну облигацию",
cleanPrice: "Без НКД",
dirtyPrice: "С НКД, без комиссии",
indicativePrice:
"Денежная оценка рассчитана по текущему номиналу и НКД. Котировка может относиться к более ранним торгам.",
percentagePoints: "п. п.",
coupons: "Купоны",
upcomingCoupons: "Ближайшие купоны",
couponHistory: "Прошедшие и недатированные купоны",
couponNumber: "Купон № {{number}}",
perBond: "На одну облигацию",
couponDays: "Купонный период: {{count}} дн.",
couponNote:
"Календарь выпуска, не личные зачисления. Оценка по текущей позиции не подтверждает право на выплату и не учитывает налоги и будущие сделки. Неизвестная сумма не означает нулевой купон.",
returnNote:
"Изменение стоимости открытой позиции по ценам портфеля. Не включает выплаченные купоны и результат продаж; это не доходность к погашению.",
noCoupons: "В выбранном периоде известных купонов нет.",
metrics: "Купон, рейтинг и доходность",
creditRating: "Кредитный рейтинг НКР",
ratingWithdrawn: "Отозван",
ratingUnavailable: "Источник недоступен",
ratingMissing: "В реестре НКР не найден",
ratingSource: "НКР · рейтинг выпуска от {{date}}",
ratingChecked: "Реестр получен {{date}}",
ratingCoverage:
"Источник — реестр эмиссий НКР. Рейтинг сопоставлен по ISIN; отсутствие записи не исключает рейтингов других агентств.",
couponAmount: "Размер ближайшего купона",
couponType: "Тип купона",
couponDate: "Дата выплаты купона",
effectiveYield: "Эффективная доходность, годовых",
couponYield: "Текущая купонная доходность, годовых",
yieldUnavailable:
"MOEX не предоставила доходность для этого выпуска и режима торгов.",
yieldSource:
"MOEX · {{target}} {{date}} · цена расчёта {{price}} % · торги {{at}}. Данные могут поступать с задержкой.",
yieldTargets: {
MATDATE: "к погашению",
OFFERDATE: "к оферте",
PUTOPTIONDATE: "к оферте",
CALLOPTIONDATE: "к досрочному погашению",
unknown: "к дате",
},
couponYieldNote:
"Купонная доходность = ближайший купон × 365 / дней в купонном периоде / цена без НКД × 100 %. Это годовая оценка при сохранении размера купона и номинала, без налогов, комиссий и реинвестирования; для плавающего или переменного купона будущий доход изменится.",
couponYieldPriceAt:
"Для купонной доходности использована котировка Т-Банка на {{date}}.",
metricsPartial:
"Часть показателей временно недоступна. Обновите раздел, чтобы повторить запрос.",
couponTypes: {
COUPON_TYPE_CONSTANT: "Постоянный",
COUPON_TYPE_FLOATING: "Плавающий",
COUPON_TYPE_DISCOUNT: "Дисконтный",
COUPON_TYPE_MORTGAGE: "Ипотечный",
COUPON_TYPE_FIX: "Фиксированный",
COUPON_TYPE_VARIABLE: "Переменный",
COUPON_TYPE_OTHER: "Другой",
},
},
stock: {
searchTitle: "Акции",
searchSubtitle:
+1
View File
@@ -15,6 +15,7 @@ const AppShell: FC<AppShellProps> = (props) => {
const items = [
{ to: "/accounts", label: t("navigation.accounts") },
{ to: "/stocks", label: t("navigation.stocks") },
{ to: "/bonds", label: t("navigation.bonds") },
{ to: "/settings", label: t("navigation.settings") },
];