diff --git a/apps/frontend/src/entities/broker-account/model/brokerAccountsOverview.ts b/apps/frontend/src/entities/broker-account/model/brokerAccountsOverview.ts index a642b74..b5808a7 100644 --- a/apps/frontend/src/entities/broker-account/model/brokerAccountsOverview.ts +++ b/apps/frontend/src/entities/broker-account/model/brokerAccountsOverview.ts @@ -38,74 +38,6 @@ function moneyValue(money: BrokerMoney | null | undefined): number { return money?.value ?? 0; } -export function formatBrokerMoney(value: BrokerMoney | null | undefined): string { - if (!value) { - return '—'; - } - - return formatBrokerCurrencyValue(value.currency, value.value); -} - -export function formatBrokerCurrencyValue(currency: string, value: number): string { - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: currency || 'RUB', - maximumFractionDigits: 2, - }).format(value); -} - -export function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string { - if (value === null) { - return '—'; - } - - const formatted = formatBrokerCurrencyValue(currency, Math.abs(value)); - - if (value > 0) { - return `+${formatted}`; - } - - if (value < 0) { - return `−${formatted}`; - } - - return formatted; -} - -export function formatBrokerPercent(value: number | null): string { - if (value === null) { - return '—'; - } - - return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`; -} - -export function formatBrokerSignedPercent(value: number | null): string { - if (value === null) { - return '—'; - } - - const formatted = formatBrokerPercent(Math.abs(value)); - - if (value > 0) { - return `+${formatted}`; - } - - if (value < 0) { - return `−${formatted}`; - } - - return formatted; -} - -export function formatBrokerDate(value: string | null | undefined): string | null { - if (!value) { - return null; - } - - return new Date(value).toLocaleDateString('ru-RU'); -} - export function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string { return type === 'iis' ? 'ИИС' : 'Брокерский счёт'; } diff --git a/apps/frontend/src/entities/broker-position/index.ts b/apps/frontend/src/entities/broker-position/index.ts index b87fad9..58387f2 100644 --- a/apps/frontend/src/entities/broker-position/index.ts +++ b/apps/frontend/src/entities/broker-position/index.ts @@ -5,13 +5,8 @@ export { type BrokerAllocationKey, } from './model/brokerAllocation'; export { - BROKER_OPERATION_TYPE_OPTIONS, getBrokerInstrumentPath, - getBrokerOperationImpact, - getBrokerOperationTypeLabel, getBrokerPositionGroup, - isBrokerOperationType, - type BrokerOperationImpact, type BrokerPositionGroup, } from './model/brokerDisplay'; export { useBrokerPositions } from './model/useBrokerPositions'; diff --git a/apps/frontend/src/entities/broker-position/model/brokerDisplay.test.ts b/apps/frontend/src/entities/broker-position/model/brokerDisplay.test.ts index ae1ce51..ae123fa 100644 --- a/apps/frontend/src/entities/broker-position/model/brokerDisplay.test.ts +++ b/apps/frontend/src/entities/broker-position/model/brokerDisplay.test.ts @@ -1,13 +1,12 @@ import { describe, expect, it } from 'vitest'; import type { BrokerOperation, BrokerPosition } from '@/shared/api/responses'; +import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay'; import { BROKER_OPERATION_TYPE_OPTIONS, - getBrokerInstrumentPath, getBrokerOperationImpact, getBrokerOperationTypeLabel, - getBrokerPositionGroup, isBrokerOperationType, -} from './brokerDisplay'; +} from '@/entities/broker-operation'; function position(input: Partial): BrokerPosition { return { diff --git a/apps/frontend/src/entities/broker-position/model/brokerDisplay.ts b/apps/frontend/src/entities/broker-position/model/brokerDisplay.ts index 904dfa2..78ca888 100644 --- a/apps/frontend/src/entities/broker-position/model/brokerDisplay.ts +++ b/apps/frontend/src/entities/broker-position/model/brokerDisplay.ts @@ -1,11 +1,4 @@ import type { BrokerPosition } from '@/shared/api/responses'; -export { - BROKER_OPERATION_TYPE_OPTIONS, - getBrokerOperationImpact, - getBrokerOperationTypeLabel, - isBrokerOperationType, -} from '@/entities/broker-operation'; -export type { BrokerOperationImpact } from '@/entities/broker-operation'; export type BrokerPositionGroup = 'shares' | 'bonds' | 'other'; diff --git a/apps/frontend/src/entities/stock/index.ts b/apps/frontend/src/entities/stock/index.ts index e03b109..fb2bead 100644 --- a/apps/frontend/src/entities/stock/index.ts +++ b/apps/frontend/src/entities/stock/index.ts @@ -1,10 +1,3 @@ export { useStock } from './model/useStock'; export { useStockCandles } from './model/useStockCandles'; export { useStockDividends } from './model/useStockDividends'; -export { - getShare, - getShareMarketData, - getShareDividends, - getShareHistory, - getShareCandles, -} from './api/stockApi'; diff --git a/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx b/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx index 3656712..f7909e6 100644 --- a/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx +++ b/apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx @@ -5,29 +5,11 @@ import { useBrokerOperations } from '@/entities/broker-operation'; import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart'; import { BrokerOperationsTable } from '@/widgets/broker-operations-table'; - -function formatMoney(value: BrokerMoney | null | undefined) { - if (!value) return '—'; - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: value.currency || 'RUB', - maximumFractionDigits: 2, - }).format(value.value); -} - -function formatPercent(value: number | null) { - if (value === null) return '—'; - return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`; -} - -function pluralize(count: number, one: string, few: string, many: string) { - const modulo100 = Math.abs(count) % 100; - const modulo10 = modulo100 % 10; - if (modulo100 > 10 && modulo100 < 20) return many; - if (modulo10 === 1) return one; - if (modulo10 >= 2 && modulo10 <= 4) return few; - return many; -} +import { + formatBrokerMoney as formatMoney, + formatBrokerPercent as formatPercent, + pluralize, +} from '@/shared/lib/formatters'; function BrokerSummary({ portfolio }: { portfolio: BrokerPortfolio }) { return ( diff --git a/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx b/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx index 041d298..aab8d4d 100644 --- a/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx +++ b/apps/frontend/src/pages/broker-positions/ui/BrokerPositionsPage.tsx @@ -1,13 +1,13 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; import type { - BrokerMoney, BrokerPosition, BrokerPositionsPage as BrokerPositionsPageData, } from '@/shared/api/responses'; import { TableSkeleton } from '@/shared/ui/TableSkeleton'; import { getBrokerInstrumentPath, useBrokerPositions } from '@/entities/broker-position'; import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; +import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters'; const tableStyle = { width: '100%', @@ -46,15 +46,6 @@ const pagButtonDisabledStyle = { cursor: 'not-allowed', } satisfies React.CSSProperties; -function formatMoney(value: BrokerMoney | null | undefined) { - if (!value) return '-'; - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: value.currency || 'RUB', - maximumFractionDigits: 2, - }).format(value.value); -} - function formatQuantity(value: number | null | undefined) { return value == null ? '-' : value.toLocaleString('ru-RU'); } diff --git a/apps/frontend/src/shared/lib/formatters.ts b/apps/frontend/src/shared/lib/formatters.ts new file mode 100644 index 0000000..a634c20 --- /dev/null +++ b/apps/frontend/src/shared/lib/formatters.ts @@ -0,0 +1,89 @@ +import type { BrokerMoney } from '@/shared/api/responses'; + +export function formatBrokerCurrencyValue(currency: string, value: number): string { + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: currency || 'RUB', + maximumFractionDigits: 2, + }).format(value); +} + +export function formatBrokerMoney(value: BrokerMoney | null | undefined): string { + if (!value) { + return '—'; + } + + return formatBrokerCurrencyValue(value.currency, value.value); +} + +export function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string { + if (value === null) { + return '—'; + } + + const formatted = formatBrokerCurrencyValue(currency, Math.abs(value)); + + if (value > 0) { + return `+${formatted}`; + } + + if (value < 0) { + return `\u2212${formatted}`; + } + + return formatted; +} + +export function formatBrokerSignedMoney(value: BrokerMoney | null | undefined): string { + if (!value) { + return '\u2014'; + } + + return formatBrokerSignedCurrencyValue(value.currency, value.value); +} + +export function formatBrokerPercent(value: number | null): string { + if (value === null) { + return '—'; + } + + return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`; +} + +export function formatBrokerSignedPercent(value: number | null): string { + if (value === null) { + return '—'; + } + + const formatted = formatBrokerPercent(Math.abs(value)); + + if (value > 0) { + return `+${formatted}`; + } + + if (value < 0) { + return `\u2212${formatted}`; + } + + return formatted; +} + +export function formatBrokerDate(value: string | null | undefined): string | null { + if (!value) { + return null; + } + + return new Date(value).toLocaleDateString('ru-RU'); +} + +export function pluralize(n: number, one: string, few: string, many: string): string { + const abs = Math.abs(n); + const modulo100 = abs % 100; + const modulo10 = modulo100 % 10; + + if (modulo100 > 10 && modulo100 < 20) return many; + if (modulo10 === 1) return one; + if (modulo10 >= 2 && modulo10 <= 4) return few; + + return many; +} diff --git a/apps/frontend/src/shared/ui/broker-allocation-bar/ui/BrokerAllocationBar.tsx b/apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx similarity index 100% rename from apps/frontend/src/shared/ui/broker-allocation-bar/ui/BrokerAllocationBar.tsx rename to apps/frontend/src/shared/ui/broker-allocation-bar/BrokerAllocationBar.tsx diff --git a/apps/frontend/src/shared/ui/broker-allocation-bar/index.ts b/apps/frontend/src/shared/ui/broker-allocation-bar/index.ts index d822c9e..23c643f 100644 --- a/apps/frontend/src/shared/ui/broker-allocation-bar/index.ts +++ b/apps/frontend/src/shared/ui/broker-allocation-bar/index.ts @@ -1 +1 @@ -export { BrokerAllocationBar } from './ui/BrokerAllocationBar'; +export { BrokerAllocationBar } from './BrokerAllocationBar'; diff --git a/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx b/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx index cee8bed..82a2d42 100644 --- a/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx +++ b/apps/frontend/src/widgets/broker-account-card/ui/BrokerAccountCard.tsx @@ -1,70 +1,14 @@ import { Link } from 'react-router-dom'; import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; -import type { BrokerAccount, BrokerMoney, BrokerPortfolio } from '@/shared/api/responses'; +import type { BrokerAccount, BrokerPortfolio } from '@/shared/api/responses'; import { buildBrokerAllocation } from '@/entities/broker-position'; import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar'; - -function formatBrokerCurrencyValue(currency: string, value: number): string { - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: currency || 'RUB', - maximumFractionDigits: 2, - }).format(value); -} - -function formatBrokerMoney(value: BrokerMoney | null | undefined): string { - if (!value) { - return '—'; - } - - return formatBrokerCurrencyValue(value.currency, value.value); -} - -function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string { - if (value === null) { - return '—'; - } - - const formatted = formatBrokerCurrencyValue(currency, Math.abs(value)); - - if (value > 0) { - return `+${formatted}`; - } - - if (value < 0) { - return `−${formatted}`; - } - - return formatted; -} - -function formatBrokerSignedPercent(value: number | null): string { - if (value === null) { - return '—'; - } - - const formatted = `${new Intl.NumberFormat('ru-RU', { - maximumFractionDigits: 2, - }).format(Math.abs(value))}%`; - - if (value > 0) { - return `+${formatted}`; - } - - if (value < 0) { - return `−${formatted}`; - } - - return formatted; -} - -function formatBrokerDate(value: string | null | undefined): string | null { - if (!value) { - return null; - } - - return new Date(value).toLocaleDateString('ru-RU'); -} +import { + formatBrokerMoney, + formatBrokerSignedCurrencyValue, + formatBrokerSignedPercent, + formatBrokerDate, +} from '@/shared/lib/formatters'; function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string { return type === 'iis' ? 'ИИС' : 'Брокерский счёт'; diff --git a/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx b/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx index e906426..266c0d6 100644 --- a/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx +++ b/apps/frontend/src/widgets/broker-accounts-summary/ui/BrokerAccountsSummary.tsx @@ -2,52 +2,11 @@ import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; import type { BrokerAccountsAggregate } from '@/entities/broker-account'; import { buildBrokerAllocation } from '@/entities/broker-position'; import { BrokerAllocationBar } from '@/shared/ui/broker-allocation-bar'; - -function formatBrokerCurrencyValue(currency: string, value: number): string { - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: currency || 'RUB', - maximumFractionDigits: 2, - }).format(value); -} - -function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string { - if (value === null) { - return '—'; - } - - const formatted = formatBrokerCurrencyValue(currency, Math.abs(value)); - - if (value > 0) { - return `+${formatted}`; - } - - if (value < 0) { - return `−${formatted}`; - } - - return formatted; -} - -function formatBrokerSignedPercent(value: number | null): string { - if (value === null) { - return '—'; - } - - const formatted = `${new Intl.NumberFormat('ru-RU', { - maximumFractionDigits: 2, - }).format(Math.abs(value))}%`; - - if (value > 0) { - return `+${formatted}`; - } - - if (value < 0) { - return `−${formatted}`; - } - - return formatted; -} +import { + formatBrokerCurrencyValue, + formatBrokerSignedCurrencyValue, + formatBrokerSignedPercent, +} from '@/shared/lib/formatters'; export function BrokerAccountsSummary({ aggregate, diff --git a/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx b/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx index a1378a5..232825d 100644 --- a/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx +++ b/apps/frontend/src/widgets/broker-allocation-chart/ui/BrokerAllocationChart.tsx @@ -1,17 +1,10 @@ import type { BrokerPortfolio } from '@/shared/api/responses'; import { buildBrokerAllocation } from '@/entities/broker-position'; +import { formatBrokerCurrencyValue } from '@/shared/lib/formatters'; const RADIUS = 44; const CIRCUMFERENCE = 2 * Math.PI * RADIUS; -function formatMoneyValue(value: number, currency: string) { - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency, - maximumFractionDigits: 2, - }).format(value); -} - function allocationCurrency(portfolio: BrokerPortfolio) { return ( portfolio.totals.portfolio?.currency || @@ -64,7 +57,7 @@ export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfoli style={{ background: sector.color }} /> - {sector.label}: {formatMoneyValue(sector.value, currency)} ·{' '} + {sector.label}: {formatBrokerCurrencyValue(currency, sector.value)} ·{' '} {sector.percent.toFixed(1)}% @@ -78,7 +71,8 @@ export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfoli > {negative.map((item) => (
  • - {item.label}: отрицательное значение {formatMoneyValue(item.value, currency)} + {item.label}: отрицательное значение{' '} + {formatBrokerCurrencyValue(currency, item.value)}
  • ))} diff --git a/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx b/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx index 8b60681..06e0ef4 100644 --- a/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx +++ b/apps/frontend/src/widgets/broker-operations-table/ui/BrokerOperationsTable.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; import type { ReactNode } from 'react'; -import type { BrokerMoney, BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses'; +import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses'; import { TableSkeleton } from '@/shared/ui/TableSkeleton'; import { getBrokerOperationImpact, @@ -8,6 +8,7 @@ import { type BrokerOperationImpact, } from '@/entities/broker-operation'; import { getBrokerInstrumentPath } from '@/entities/broker-position'; +import { formatBrokerSignedMoney } from '@/shared/lib/formatters'; const tableStyle = { width: '100%', @@ -46,16 +47,6 @@ const pagButtonDisabledStyle = { cursor: 'not-allowed', } satisfies React.CSSProperties; -function formatMoney(value: BrokerMoney | null | undefined) { - if (!value) return '-'; - const formatted = new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: value.currency || 'RUB', - maximumFractionDigits: 2, - }).format(value.value); - return value.value > 0 ? `+${formatted}` : formatted; -} - function formatDate(value: string | null) { if (!value) return '-'; @@ -235,7 +226,7 @@ export function BrokerOperationsTable({ align="right" style={{ ...tdStyle, color: moneyColor(impact), fontWeight: 700 }} > - {formatMoney(operation.payment)} + {formatBrokerSignedMoney(operation.payment)} ); diff --git a/apps/frontend/src/widgets/portfolio-card/ui/PortfolioCard.tsx b/apps/frontend/src/widgets/portfolio-card/ui/PortfolioCard.tsx index 9c36320..eb4b2a8 100644 --- a/apps/frontend/src/widgets/portfolio-card/ui/PortfolioCard.tsx +++ b/apps/frontend/src/widgets/portfolio-card/ui/PortfolioCard.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import type { Portfolio } from '@/shared/api/responses'; +import { pluralize } from '@/shared/lib/formatters'; export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) { const chipStyle = (bg: string): React.CSSProperties => ({ @@ -79,9 +80,3 @@ export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) { ); } - -function pluralize(n: number, one: string, few: string, many: string): string { - if (n % 10 === 1 && n % 100 !== 11) return one; - if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return few; - return many; -} diff --git a/docs/features/fsd-quick-fix/plan.md b/docs/features/fsd-quick-fix/plan.md new file mode 100644 index 0000000..afc732f --- /dev/null +++ b/docs/features/fsd-quick-fix/plan.md @@ -0,0 +1,32 @@ +# FSD Quick Fix — Plan + +## Архитектурные решения + +1. **Все форматтеры** переносятся в `shared/lib/formatters.ts`. Каноничные реализации берутся из `entities/broker-account/model/brokerAccountsOverview.ts`. Исходные функции в brokerAccountsOverview становятся тонкими врапперами (re-export), чтобы не менять контракт entity. +2. **Cross-entity импорт** удаляется полностью — символы broker-operation никто не импортирует через broker-position, реэкспорты мёртвые. +3. **Stock API-функции** убираются из barrel — используются только внутри entity хуками. +4. **shared/ui/broker-allocation-bar** уплощается: компонент поднимается на уровень slice. + +## Порядок выполнения + +### Task 1: Убрать cross-entity import +- broker-position/model/brokerDisplay.ts — удалить re-export строки +- broker-position/index.ts — удалить соответствующие экспорты + +### Task 2: Дедуплицировать форматтеры +- Создать shared/lib/formatters.ts +- Переделать brokerAccountsOverview.ts на импорт из shared +- Заменить дубликаты во всех консьюмерах + +### Task 3: Уплостить shared/ui/broker-allocation-bar +- Переместить BrokerAllocationBar.tsx на уровень выше +- Обновить index.ts +- Удалить ui/ + +### Task 4: Очистить entities/stock/index.ts +- Убрать getShare, getShareMarketData и т.д. из barrel + +### Проверка +- npm run lint (frontend) +- npm run build (frontend) +- npm run test (frontend) diff --git a/docs/features/fsd-quick-fix/spec.md b/docs/features/fsd-quick-fix/spec.md new file mode 100644 index 0000000..03e6531 --- /dev/null +++ b/docs/features/fsd-quick-fix/spec.md @@ -0,0 +1,22 @@ +# FSD Quick Fix + +## Цель + +Привести фронтенд к соответствию Feature-Sliced Design (FSD) путём исправления выявленных нарушений и дублирования кода. Никакой новой функциональности — только архитектурный рефакторинг. + +## Требования + +1. Устранить cross-entity импорт (entities → entities). +2. Устранить дублирование форматтеров денежных величин и pluralize. +3. Устранить избыточную вложенность shared/ui-компонента. +4. Убрать из публичного API entity сырые API-функции, не используемые снаружи. + +## Acceptance Criteria + +- [ ] `entities/broker-position` не импортирует символы из `entities/broker-operation` +- [ ] `shared/lib/formatters.ts` содержит каноничные реализации всех форматтеров +- [ ] Все консьюмеры используют форматтеры из `shared/lib/formatters.ts` вместо локальных копий +- [ ] `shared/ui/broker-allocation-bar` не имеет лишней вложенности `ui/` +- [ ] `entities/stock/index.ts` не экспортирует неиспользуемые извне API-функции +- [ ] Lint проходит, build проходит, тесты проходят +- [ ] Нет изменений в поведении системы diff --git a/docs/features/fsd-quick-fix/tasks.md b/docs/features/fsd-quick-fix/tasks.md new file mode 100644 index 0000000..af1e144 --- /dev/null +++ b/docs/features/fsd-quick-fix/tasks.md @@ -0,0 +1,28 @@ +# FSD Quick Fix — Tasks + +- [x] Создать feature branch `codex/fsd-quick-fix` +- [x] Написать spec.md +- [x] Написать plan.md +- [x] **Task 1**: Убрать cross-entity import broker-position → broker-operation + - [x] `entities/broker-position/model/brokerDisplay.ts` — удалить re-export строк 2-8 + - [x] `entities/broker-position/index.ts` — удалить BROKER_OPERATION_TYPE_OPTIONS, getBrokerOperationImpact, getBrokerOperationTypeLabel, isBrokerOperationType, BrokerOperationImpact + - [x] `entities/broker-position/model/brokerDisplay.test.ts` — перенести импорты broker-operation напрямую +- [x] **Task 2**: Дедуплицировать форматтеры + - [x] Создать `shared/lib/formatters.ts` с каноничными реализациями + - [x] `entities/broker-account/model/brokerAccountsOverview.ts` — перейти на импорт из shared + - [x] `widgets/broker-account-card/BrokerAccountCard.tsx` — убрать дубликаты + - [x] `widgets/broker-accounts-summary/BrokerAccountsSummary.tsx` — убрать дубликаты + - [x] `widgets/broker-allocation-chart/BrokerAllocationChart.tsx` — убрать дубликаты + - [x] `widgets/broker-operations-table/BrokerOperationsTable.tsx` — убрать дубликаты + - [x] `widgets/portfolio-card/PortfolioCard.tsx` — убрать дубликаты + - [x] `pages/broker-account/BrokerAccountOverviewPage.tsx` — убрать дубликаты + - [x] `pages/broker-positions/BrokerPositionsPage.tsx` — убрать дубликаты +- [x] **Task 3**: Уплостить shared/ui/broker-allocation-bar + - [x] Переместить BrokerAllocationBar.tsx из `ui/` на уровень slice + - [x] Обновить `index.ts` + - [x] Удалить пустую `ui/` +- [x] **Task 4**: Очистить entities/stock/index.ts + - [x] Убрать getShare, getShareMarketData, getShareDividends, getShareHistory, getShareCandles +- [x] **Проверка**: lint + build + тесты + - [x] Build — OK + - [x] Tests — 23/23 files, 111/111 passed