docs: translate docs to russian #14

Merged
ksv741 merged 1 commits from codex/docs-sdd-cleanup into main 2026-06-16 05:15:15 +03:00
39 changed files with 797 additions and 495 deletions

View File

@ -1,13 +1,13 @@
# ADR-001: Backend — Single Point of Access to MOEX # ADR-001: Backend — единственная точка доступа к MOEX
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect, Tech Lead **Участники решения:** Architect, Tech Lead
## Context ## Контекст
Frontend должен отображать данные Московской биржи. MOEX ISS API отдаёт сырые данные со сложной структурой (вложенные таблицы, различные форматы). Прямые запросы с фронта приведут к дублированию логики нормализации, усложнят обработку ошибок и сделают систему зависимой от внешнего API. Frontend должен отображать данные Московской биржи. MOEX ISS API отдаёт сырые данные со сложной структурой (вложенные таблицы, различные форматы). Прямые запросы с фронта приведут к дублированию логики нормализации, усложнят обработку ошибок и сделают систему зависимой от внешнего API.
## Decision ## Решение
Backend (NestJS) является единственной точкой доступа к MOEX. Frontend никогда не обращается к MOEX напрямую. Backend (NestJS) является единственной точкой доступа к MOEX. Frontend никогда не обращается к MOEX напрямую.
Backend: Backend:
@ -17,7 +17,7 @@ Backend:
- Обрабатывает ошибки MOEX (пустые данные, rate limit, таймауты) - Обрабатывает ошибки MOEX (пустые данные, rate limit, таймауты)
- Предоставляет собственный OpenAPI-контракт для фронта - Предоставляет собственный OpenAPI-контракт для фронта
## Consequences ## Последствия
- Единый источник правды для трансформации данных - Единый источник правды для трансформации данных
- Изоляция изменений MOEX API — меняется только MoexClient - Изоляция изменений MOEX API — меняется только MoexClient
- Централизованное кеширование сокращает количество запросов к MOEX - Централизованное кеширование сокращает количество запросов к MOEX

View File

@ -1,13 +1,13 @@
# ADR-002: In-Memory Cache with Migration Path to Redis # ADR-002: In-memory cache с путём миграции на Redis
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect, Tech Lead **Участники решения:** Architect, Tech Lead
## Context ## Контекст
Для MVP требуется кеширование MOEX-данных, чтобы снизить нагрузку на внешнее API и обеспечить приемлемое время ответа. На начальном этапе нет требований к горизонтальному масштабированию, и хочется избежать внешних зависимостей. Для MVP требуется кеширование MOEX-данных, чтобы снизить нагрузку на внешнее API и обеспечить приемлемое время ответа. На начальном этапе нет требований к горизонтальному масштабированию, и хочется избежать внешних зависимостей.
## Decision ## Решение
Использовать `@nestjs/cache-manager` с MemoryStore. TTL настраивается per-endpoint через конфигурацию. Использовать `@nestjs/cache-manager` с MemoryStore. TTL настраивается per-endpoint через конфигурацию.
Архитектура позволяет переключиться на Redis заменой импорта провайдера: Архитектура позволяет переключиться на Redis заменой импорта провайдера:
@ -26,7 +26,7 @@ CacheModule.registerAsync({
}) })
``` ```
## Consequences ## Последствия
- Нет внешних зависимостей для MVP - Нет внешних зависимостей для MVP
- Кеш сбрасывается при рестарте сервера (приемлемо для read-only приложения) - Кеш сбрасывается при рестарте сервера (приемлемо для read-only приложения)
- Чистый путь миграции на Redis - Чистый путь миграции на Redis

View File

@ -1,20 +1,20 @@
# ADR-003: Rate Limiting Strategy for MOEX Client # ADR-003: Стратегия rate limiting для MOEX client
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect, Tech Lead **Участники решения:** Architect, Tech Lead
## Context ## Контекст
MOEX ISS не документирует жёсткие лимиты на количество запросов, но массовые запросы могут привести к блокировке или ухудшению качества обслуживания. Backend является единственным клиентом MOEX и должен контролировать исходящий трафик. MOEX ISS не документирует жёсткие лимиты на количество запросов, но массовые запросы могут привести к блокировке или ухудшению качества обслуживания. Backend является единственным клиентом MOEX и должен контролировать исходящий трафик.
## Decision ## Решение
Внедрить два механизма в MoexClient: Внедрить два механизма в MoexClient:
1. **Request Queue (p-queue)**: конфигурируемый лимит запросов в секунду (default: 10 req/s). Запросы сверх лимита ставятся в очередь и выполняются по расписанию. 1. **Request queue (p-queue)**: конфигурируемый лимит запросов в секунду (по умолчанию: 10 req/s). Запросы сверх лимита ставятся в очередь и выполняются по расписанию.
2. **Circuit Breaker (`@nestjs/axios` + interceptor)**: при 5+ последовательных ошибках (5xx, timeout, network error) клиент перестаёт отправлять запросы к MOEX на 30 секунд. После таймаута — пробный запрос для восстановления. 2. **Circuit Breaker (`@nestjs/axios` + interceptor)**: при 5+ последовательных ошибках (5xx, timeout, network error) клиент перестаёт отправлять запросы к MOEX на 30 секунд. После таймаута — пробный запрос для восстановления.
## Consequences ## Последствия
- Плавная нагрузка на MOEX, без пиков - Плавная нагрузка на MOEX, без пиков
- Автоматическое восстановление после сбоев MOEX - Автоматическое восстановление после сбоев MOEX
- Graceful degradation: при отключённом circuit breaker возвращаются кешированные данные - Graceful degradation: при отключённом circuit breaker возвращаются кешированные данные

View File

@ -1,16 +1,16 @@
# ADR-004: Feature Modules by Domain # ADR-004: Feature-модули по доменам
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect, Tech Lead **Участники решения:** Architect, Tech Lead
## Context ## Контекст
NestJS рекомендует модульную архитектуру. Требования указывают на архитектуру по feature modules. Модули должны иметь чёткие границы и быть тестируемыми изолированно. NestJS рекомендует модульную архитектуру. Требования указывают на архитектуру по feature modules. Модули должны иметь чёткие границы и быть тестируемыми изолированно.
## Decision ## Решение
Каждый бизнес-домен — отдельный NestJS feature module: Каждый бизнес-домен — отдельный NestJS feature module:
| Module | Responsibility | | Модуль | Ответственность |
|--------|---------------| |--------|---------------|
| `MoexClientModule` | HTTP-клиент к MOEX ISS, rate limiting, circuit breaker | | `MoexClientModule` | HTTP-клиент к MOEX ISS, rate limiting, circuit breaker |
| `CacheModule` | Абстракция кеширования | | `CacheModule` | Абстракция кеширования |
@ -22,7 +22,7 @@ NestJS рекомендует модульную архитектуру. Тре
Каждый module exports свой сервис, control imports через `@Module({ imports: [...] })`. Каждый module exports свой сервис, control imports через `@Module({ imports: [...] })`.
## Consequences ## Последствия
- Чёткие границы, изолированное тестирование - Чёткие границы, изолированное тестирование
- Возможность вынести модуль в отдельный микросервис - Возможность вынести модуль в отдельный микросервис
- Понятная навигация по коду - Понятная навигация по коду

View File

@ -1,13 +1,13 @@
# ADR-005: OpenAPI Codegen with openapi-typescript # ADR-005: OpenAPI codegen через openapi-typescript
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect, Tech Lead **Участники решения:** Architect, Tech Lead
## Context ## Контекст
Frontend должен потреблять API бэкенда. Ручное написание клиентов и DTO приводит к рассинхронизации с бэкендом и ошибкам типизации. Frontend должен потреблять API бэкенда. Ручное написание клиентов и DTO приводит к рассинхронизации с бэкендом и ошибкам типизации.
## Decision ## Решение
Использовать `openapi-typescript` + `openapi-fetch` для генерации: Использовать `openapi-typescript` + `openapi-fetch` для генерации:
- TypeScript типов (DTO, request/response schemas) - TypeScript типов (DTO, request/response schemas)
@ -33,7 +33,7 @@ export function useStock(secid: string) {
} }
``` ```
## Consequences ## Последствия
- Полная типобезопасность на стыке frontend/backend - Полная типобезопасность на стыке frontend/backend
- Автоматическая синхронизация с API-контрактом - Автоматическая синхронизация с API-контрактом
- TanStack Query hooks пишутся вручную — полный контроль staleTime/caching - TanStack Query hooks пишутся вручную — полный контроль staleTime/caching

View File

@ -1,20 +1,20 @@
# ADR-006: CCI (Financial Reporting) Moved Out of MVP # ADR-006: CCI (финансовая отчётность) вынесен за пределы MVP
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect, Product **Участники решения:** Architect, Product
## Context ## Контекст
MOEX предоставляет корпоративную информацию (CCI) — финансовую отчётность по МСФО/РСБУ. Данные включают отчёты о прибылях/убытках, балансовые отчёты, мультипликаторы. Однако: MOEX предоставляет корпоративную информацию (CCI) — финансовую отчётность по МСФО/РСБУ. Данные включают отчёты о прибылях/убытках, балансовые отчёты, мультипликаторы. Однако:
- CCI API имеет собственную сложную структуру (виды отчётности, периоды, индикаторы) - CCI API имеет собственную сложную структуру (виды отчётности, периоды, индикаторы)
- Данные требуют дополнительной нормализации и расчёта метрик - Данные требуют дополнительной нормализации и расчёта метрик
- Для MVP пользователи хотят базовую информацию (цена, купон, график) - Для MVP пользователи хотят базовую информацию (цена, купон, график)
## Decision ## Решение
Не включать CCI в MVP. Roadmap на post-MVP. Не включать CCI в MVP. Roadmap на post-MVP.
## Consequences ## Последствия
- Меньший объём работы в MVP - Меньший объём работы в MVP
- API не привязывается к CCI-схемам (будет отдельный модуль) - API не привязывается к CCI-схемам (будет отдельный модуль)
- Пользователи не увидят мультипликаторы (P/E, EV/EBITDA) в первой версии - Пользователи не увидят мультипликаторы (P/E, EV/EBITDA) в первой версии

View File

@ -1,15 +1,15 @@
# ADR-007: Two-Level Caching (Backend + Frontend) # ADR-007: Двухуровневое кеширование (backend + frontend)
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-13 **Дата:** 2026-06-13
**Deciders:** Architect **Участники решения:** Architect
## Context ## Контекст
Данные MOEX имеют задержку 15 минут. Кеширование на одном уровне (только бэкенд или только фронтенд) неоптимально: Данные MOEX имеют задержку 15 минут. Кеширование на одном уровне (только бэкенд или только фронтенд) неоптимально:
- Только бэкенд: каждый пользователь создаёт запрос к серверу - Только бэкенд: каждый пользователь создаёт запрос к серверу
- Только фронтенд: нет централизованного кеша, не защищает MOEX от повторных запросов - Только фронтенд: нет централизованного кеша, не защищает MOEX от повторных запросов
## Decision ## Решение
Внедрить два уровня кеширования: Внедрить два уровня кеширования:
1. **Backend (in-memory cache-manager)**: централизованное кеширование ответов от MOEX. Предотвращает повторные запросы к MOEX от разных пользователей. 1. **Backend (in-memory cache-manager)**: централизованное кеширование ответов от MOEX. Предотвращает повторные запросы к MOEX от разных пользователей.
@ -20,7 +20,7 @@ TTL согласованы (см. Caching Strategy).
Cache-Control заголовки в HTTP-ответах для промежуточных proxy/CDN (опционально). Cache-Control заголовки в HTTP-ответах для промежуточных proxy/CDN (опционально).
## Consequences ## Последствия
- Избыточность intentional: resilience при отказе одного уровня - Избыточность intentional: resilience при отказе одного уровня
- TanStack Query staleTime = backend TTL (нет лишних запросов) - TanStack Query staleTime = backend TTL (нет лишних запросов)
- При рестарте бэкенда фронт всё ещё имеет данные в memory cache - При рестарте бэкенда фронт всё ещё имеет данные в memory cache

View File

@ -3,13 +3,13 @@ sidebar_position: 14
title: ADR-008 title: ADR-008
--- ---
# ADR-008: Authentication & Authorization System # ADR-008: Система Authentication и Authorization
## Status ## Статус
Accepted Accepted
## Context ## Контекст
Приложение MoexVibe публичное, но для персонализации и будущих функций (избранное, уведомления, подписки) требуется система аутентификации. Необходимо решение, которое: Приложение MoexVibe публичное, но для персонализации и будущих функций (избранное, уведомления, подписки) требуется система аутентификации. Необходимо решение, которое:
@ -18,26 +18,26 @@ Accepted
- Позволяет расширять ролевую модель - Позволяет расширять ролевую модель
- Интегрируется в существующий стек (NestJS + React) - Интегрируется в существующий стек (NestJS + React)
## Decision ## Решение
### Backend ### Backend
- **Database:** SQLite через Prisma ORM (без необходимости внешнего сервиса) - **База данных:** SQLite через Prisma ORM без внешнего сервиса
- **Auth strategy:** JWT access tokens (15m) + refresh tokens (7d) in httpOnly cookies - **Auth strategy:** JWT access tokens (15m) + refresh tokens (7d) в httpOnly cookies
- **Password storage:** bcrypt with 12 salt rounds - **Хранение паролей:** bcrypt with 12 salt rounds
- **Guard model:** Global `JwtAuthGuard` (все маршруты защищены по умолчанию, `@Public()` для открытых) - **Guard model:** глобальный `JwtAuthGuard` (все маршруты защищены по умолчанию, `@Public()` для открытых)
- **RBAC:** Роли `user` и `admin`, проверка через `RolesGuard` - **RBAC:** Роли `user` и `admin`, проверка через `RolesGuard`
### Frontend ### Frontend
- **State management:** React Context (`AuthContext`) для хранения пользователя и access token - **Управление состоянием:** React Context (`AuthContext`) для хранения пользователя и access token
- **Token storage:** Access token только в памяти (не localStorage), refresh token в httpOnly cookie - **Token storage:** access token только в памяти (не localStorage), refresh token в httpOnly cookie
- **Auto-refresh:** При 401 → автоматический вызов `/auth/refresh` → повтор оригинального запроса - **Auto-refresh:** При 401 → автоматический вызов `/auth/refresh` → повтор оригинального запроса
- **Route protection:** `ProtectedRoute` компонент, редирект на `/login` с return URL - **Route protection:** компонент `ProtectedRoute`, редирект на `/login` с return URL
## Consequences ## Последствия
### Positive ### Плюсы
- httpOnly cookie защищает refresh token от XSS - httpOnly cookie защищает refresh token от XSS
- Access token в памяти защищён от кражи через localStorage - Access token в памяти защищён от кражи через localStorage
@ -45,17 +45,17 @@ Accepted
- Refresh token rotation повышает безопасность - Refresh token rotation повышает безопасность
- Глобальный guard — безопасность по умолчанию - Глобальный guard — безопасность по умолчанию
### Negative ### Минусы
- Access token теряется при полной перезагрузке страницы (восстанавливается через refresh cookie) - Access token теряется при полной перезагрузке страницы (восстанавливается через refresh cookie)
- Для production требуется генерация надёжных JWT_SECRET - Для production требуется генерация надёжных JWT_SECRET
- Нет rate limiting на login endpoint (TODO) - Rate limiting на login endpoint не входит в текущее решение
- Нет верификации email (принятое упрощение) - Нет верификации email (принятое упрощение)
## Migration Path ## Путь миграции
Для перехода на PostgreSQL потребуется: Для перехода на PostgreSQL потребуется:
1. Изменить `provider` в schema.prisma на `postgresql` 1. Изменить `provider` в schema.prisma на `postgresql`
2. Обновить `DATABASE_URL` 2. Обновить `DATABASE_URL`
3. Выполнить `prisma migrate dev` 3. Выполнить `prisma migrate dev`
4. Никаких изменений кода не требуется (Prisma abstracts database) 4. Никаких изменений кода не требуется: Prisma абстрагирует database

View File

@ -1,27 +1,29 @@
# ADR-009: Portfolio Domain Model # ADR-009: Доменная модель портфеля
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-14 **Дата:** 2026-06-14
## Context ## Контекст
Adding portfolio tracking feature to MoexVibe. Need to decide on data model — relational tables vs document-oriented storage for portfolio and position data. В MoexVibe добавляется отслеживание портфелей. Нужно выбрать модель данных: реляционные таблицы
или document-oriented storage для портфелей и позиций.
## Decision ## Решение
Use SQL via Prisma (existing database) with `Portfolio` and `Position` as separate tables. JSON fields for flexible data (`targets`, `tags`). Использовать SQL через Prisma в существующей базе: `Portfolio` и `Position` как отдельные таблицы,
JSON-поля для гибких данных (`targets`, `tags`).
## Rationale ## Обоснование
- SQLite already in use for User model - SQLite уже используется для модели User
- Portfolio and Position have clear relational structure (1:N) - Portfolio и Position имеют понятную реляционную структуру 1:N
- JSON fields cover semi-structured requirements (targets, tags) without adding another database - JSON-поля закрывают полуструктурированные требования (`targets`, `tags`) без новой базы
- No need for complex queries across tags at current scale (50 positions max per portfolio) - На текущем масштабе не нужны сложные запросы по tags (до 50 позиций на портфель)
- Straightforward migration to PostgreSQL if needed - При необходимости есть прямой путь миграции на PostgreSQL
## Consequences ## Последствия
- JSON fields not individually queryable in SQLite (acceptable for Phase 1) - JSON-поля не индексируются и не запрашиваются по отдельности в SQLite, что приемлемо для Phase 1
- Targets edited as full JSON replacement (not atomic per-item update) - Targets редактируются полной заменой JSON, без атомарного обновления отдельных элементов
- Cascade delete handles portfolio → position cleanup - Cascade delete очищает позиции при удалении портфеля

View File

@ -1,26 +1,29 @@
# ADR-010: Backend Price Computation # ADR-010: Расчёт цен на backend
**Status:** Accepted **Статус:** Accepted
**Date:** 2026-06-14 **Дата:** 2026-06-14
## Context ## Контекст
Portfolio positions need current market prices for value calculation. Where should price computation happen — on backend or frontend? Позициям портфеля нужны текущие рыночные цены для расчёта стоимости. Нужно решить, где выполнять
расчёт: на backend или frontend.
## Decision ## Решение
Compute prices on backend. `GET /api/v1/portfolios/:id` returns fully computed `PortfolioDetailResponseDto` with `currentPrice`, `currentValue`, `weightPercent`, and `deviation` for each position. Рассчитывать цены на backend. `GET /api/v1/portfolios/:id` возвращает полностью рассчитанный
`PortfolioDetailResponseDto` с `currentPrice`, `currentValue`, `weightPercent` и `deviation` для
каждой позиции.
## Rationale ## Обоснование
- Single source of truth for financial calculations - Единый источник правды для финансовых расчётов
- Frontend receives ready-to-display data - Frontend получает готовые к отображению данные
- Backend caching reduces MOEX API calls - Backend cache сокращает количество запросов к MOEX API
- Avoids N individual price requests from frontend - Frontend не делает N отдельных запросов цен
## Consequences ## Последствия
- Backend makes N MOEX requests per portfolio read (cached by TTL 900s) - Backend делает N запросов к MOEX при чтении портфеля; они кешируются с TTL 900s
- Portfolio endpoint cannot use naive response caching (prices per-user) - Portfolio endpoint не может использовать наивное response caching, потому что цены считаются для пользователя
- Extra load on backend when many users view portfolios simultaneously - При одновременном просмотре портфелей многими пользователями растёт нагрузка на backend

View File

@ -1,16 +1,16 @@
# Architecture Decision Records # Архитектурные решения (ADR)
| ADR | Status | Description | | ADR | Статус | Описание |
|---|---|---| |---|---|---|
| [ADR-001](ADR-001-backend-single-point-of-access) | Accepted | Backend — Single Point of Access to MOEX | | [ADR-001](ADR-001-backend-single-point-of-access) | Accepted | Backend — единственная точка доступа к MOEX |
| [ADR-002](ADR-002-in-memory-cache) | Accepted | In-Memory Cache Strategy | | [ADR-002](ADR-002-in-memory-cache) | Accepted | Стратегия in-memory cache |
| [ADR-003](ADR-003-rate-limiting-strategy) | Accepted | Rate Limiting Strategy | | [ADR-003](ADR-003-rate-limiting-strategy) | Accepted | Стратегия rate limiting |
| [ADR-004](ADR-004-feature-modules) | Accepted | Feature Modules Architecture | | [ADR-004](ADR-004-feature-modules) | Accepted | Архитектура feature-модулей |
| [ADR-005](ADR-005-openapi-codegen-frontend) | Accepted | OpenAPI Codegen for Frontend | | [ADR-005](ADR-005-openapi-codegen-frontend) | Accepted | OpenAPI codegen для frontend |
| [ADR-006](ADR-006-no-cci) | Deprecated | No Custom Components Infrastructure | | [ADR-006](ADR-006-no-cci) | Deprecated | CCI вынесен за пределы MVP |
| [ADR-007](ADR-007-two-level-caching) | Draft | Two-Level Caching (In-Memory + Redis) | | [ADR-007](ADR-007-two-level-caching) | Draft | Двухуровневый cache: backend + frontend |
| [ADR-008](ADR-008-auth-system) | Accepted | Authentication & Authorization | | [ADR-008](ADR-008-auth-system) | Accepted | Authentication и Authorization |
| [ADR-009](ADR-009-portfolio-domain) | Accepted | Portfolio Domain Model | | [ADR-009](ADR-009-portfolio-domain) | Accepted | Доменная модель портфеля |
| [ADR-010](ADR-010-backend-price-computation) | Accepted | Backend Price Computation | | [ADR-010](ADR-010-backend-price-computation) | Accepted | Расчёт цен на backend |
Все опубликованные ADR находятся в `apps/docs/docs/adr/` и отображаются в этом Docusaurus-разделе. Все опубликованные ADR находятся в `apps/docs/docs/adr/` и отображаются в этом Docusaurus-разделе.

View File

@ -1,94 +1,101 @@
# Architecture # Архитектура
## System Architecture ## Архитектура системы
```mermaid ```mermaid
graph TD flowchart LR
Browser["Browser<br/>(React SPA)"] Browser["Browser<br/>(React SPA)"]
Backend["NestJS API<br/>:3000"] API["NestJS API<br/>:3000"]
Cache["In-Memory Cache<br/>(cache-manager)"] Cache["In-memory cache<br/>(cache-manager)"]
MOEX["MOEX ISS API<br/>iss.moex.com"] MOEX["MOEX ISS API<br/>iss.moex.com"]
DB[("SQLite Database<br/>(Prisma)")] DB[("SQLite<br/>(Prisma)")]
Browser -->|"/api/v1/*"| Backend Browser -->|"/api/v1/*"| API
Browser -->|"Cookie: refreshToken"| Backend Browser -->|"Cookie: refreshToken"| API
Browser -->|"Authorization: Bearer"| Backend Browser -->|"Authorization: Bearer"| API
Backend -->|"getOrFetch()"| Cache API -->|"getOrFetch()"| Cache
Backend -->|"GET /iss/*.json"| MOEX API -->|"GET /iss/*.json"| MOEX
Backend -->|"Prisma ORM"| DB API -->|"Prisma ORM"| DB
Cache -->|"data"| Backend Cache -->|"данные"| API
DB -->|"users"| Backend DB -->|"пользователи и портфели"| API
MOEX -->|"raw data"| Backend MOEX -->|"сырые данные"| API
subgraph Backend_Internal["Backend (NestJS)"]
MoexClient["MoexClientModule<br/>p-queue + circuit breaker"]
Auth["AuthModule<br/>JWT + bcrypt + Guards"]
Shares["SharesModule"]
Bonds["BondsModule"]
Candles["CandlesModule"]
Securities["SecuritiesModule"]
Health["HealthModule"]
CacheService["CacheService<br/>(global)"]
Prisma["PrismaService<br/>(global)"]
Auth -->|"user CRUD"| Prisma
MoexClient -->|"fetches"| Shares
MoexClient -->|"fetches"| Bonds
MoexClient -->|"fetches"| Candles
MoexClient -->|"fetches"| Securities
Shares -->|"uses"| CacheService
Bonds -->|"uses"| CacheService
Candles -->|"uses"| CacheService
Securities -->|"uses"| CacheService
end
``` ```
## Request Flow (Authenticated) ## Внутренние модули backend
```mermaid
flowchart LR
Auth["AuthModule<br/>JWT + bcrypt + guards"]
Shares["SharesModule"]
Bonds["BondsModule"]
Candles["CandlesModule"]
Securities["SecuritiesModule"]
Portfolio["PortfolioModule"]
Health["HealthModule"]
MoexClient["MoexClientModule<br/>p-queue + circuit breaker"]
CacheService["CacheService<br/>(global)"]
Prisma["PrismaService<br/>(global)"]
Auth -->|"CRUD пользователей"| Prisma
Portfolio -->|"портфели и позиции"| Prisma
MoexClient -->|"запрашивает"| Shares
MoexClient -->|"запрашивает"| Bonds
MoexClient -->|"запрашивает"| Candles
MoexClient -->|"запрашивает"| Securities
MoexClient -->|"обогащает"| Portfolio
Shares -->|"использует"| CacheService
Bonds -->|"использует"| CacheService
Candles -->|"использует"| CacheService
Securities -->|"использует"| CacheService
Portfolio -->|"использует"| CacheService
```
## Поток авторизованного запроса
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant User participant User as Пользователь
participant Frontend as React SPA participant Frontend as React SPA
participant Backend as NestJS API participant Backend as NestJS API
participant Cache as In-Memory Cache participant Cache as In-memory cache
participant MOEX as MOEX ISS participant MOEX as MOEX ISS
participant AuthDB as SQLite participant AuthDB as SQLite
User->>Frontend: View stock SBER User->>Frontend: Открывает акцию SBER
Note over Frontend: Access token in memory Note over Frontend: access token хранится в памяти
Frontend->>Backend: GET /securities/shares/SBER (Authorization: Bearer) Frontend->>Backend: GET /securities/shares/SBER (Authorization: Bearer)
Backend->>Backend: JwtAuthGuard validates token Backend->>Backend: JwtAuthGuard проверяет token
Backend->>Cache: getOrFetch('share:SBER') Backend->>Cache: getOrFetch('share:SBER')
alt Cache miss alt cache miss
Cache->>Backend: null Cache->>Backend: null
Backend->>MOEX: GET /iss/.../SBER.json Backend->>MOEX: GET /iss/.../SBER.json
MOEX-->>Backend: raw data MOEX-->>Backend: сырые данные
Backend->>Cache: set('share:SBER', ..., TTL=900) Backend->>Cache: set('share:SBER', ..., TTL=900)
else Cache hit else cache hit
Cache-->>Backend: cached data Cache-->>Backend: cached data
end end
Backend-->>Frontend: { data, meta } Backend-->>Frontend: { data, meta }
Frontend-->>User: rendered UI Frontend-->>User: отрисованный UI
``` ```
## Architecture Decisions ## Архитектурные решения
All architectural decisions are documented as ADR pages in this documentation app: Все архитектурные решения описаны в ADR-страницах этой документации:
| ADR | Summary | | ADR | Кратко |
|---|---| |---|---|
| [ADR-001](adr/ADR-001-backend-single-point-of-access) | Backend — single point of access to MOEX | | [ADR-001](adr/ADR-001-backend-single-point-of-access) | Backend — единственная точка доступа к MOEX |
| [ADR-002](adr/ADR-002-in-memory-cache) | In-memory cache (cache-manager) | | [ADR-002](adr/ADR-002-in-memory-cache) | In-memory cache через cache-manager |
| [ADR-003](adr/ADR-003-rate-limiting-strategy) | Rate limiting with p-queue | | [ADR-003](adr/ADR-003-rate-limiting-strategy) | Rate limiting через p-queue |
| [ADR-004](adr/ADR-004-feature-modules) | Feature modules architecture | | [ADR-004](adr/ADR-004-feature-modules) | Архитектура feature-модулей |
| [ADR-005](adr/ADR-005-openapi-codegen-frontend) | OpenAPI codegen for frontend | | [ADR-005](adr/ADR-005-openapi-codegen-frontend) | OpenAPI codegen для frontend |
| [ADR-006](adr/ADR-006-no-cci) | No CCI (Custom Components Infrastructure) | | [ADR-006](adr/ADR-006-no-cci) | Отказ от CCI (Custom Components Infrastructure) |
| [ADR-007](adr/ADR-007-two-level-caching) | Two-level caching strategy | | [ADR-007](adr/ADR-007-two-level-caching) | Двухуровневая стратегия cache |
| [ADR-008](adr/ADR-008-auth-system) | Authentication & Authorization | | [ADR-008](adr/ADR-008-auth-system) | Authentication и Authorization |
| [ADR-009](adr/ADR-009-portfolio-domain) | Portfolio domain model | | [ADR-009](adr/ADR-009-portfolio-domain) | Доменная модель портфеля |
| [ADR-010](adr/ADR-010-backend-price-computation) | Backend price computation | | [ADR-010](adr/ADR-010-backend-price-computation) | Расчёт цен на backend |
## Response Format ## Формат ответа
Все ответы API обёрнуты в единый формат: Все ответы API обёрнуты в единый формат:
@ -114,9 +121,9 @@ All architectural decisions are documented as ADR pages in this documentation ap
} }
``` ```
## Global Configuration ## Глобальная конфигурация
- Global prefix: `/api/v1` - Глобальный префикс: `/api/v1`
- ValidationPipe: `transform: true, whitelist: true` - ValidationPipe: `transform: true, whitelist: true`
- HttpExceptionFilter (catch-all) - HttpExceptionFilter (catch-all)
- TransformInterceptor (авто-обёртка в `ApiResponse`) - TransformInterceptor (авто-обёртка в `ApiResponse`)

View File

@ -1,4 +1,4 @@
# API Reference # API reference
Все эндпоинты находятся под префиксом `/api/v1`. Swagger UI: `/api/docs`. Все эндпоинты находятся под префиксом `/api/v1`. Swagger UI: `/api/docs`.
@ -104,13 +104,13 @@
Поиск по инструментам. Поиск по инструментам.
**Parameters:** **Параметры:**
| Param | Type | Required | Default | Description | | Параметр | Тип | Обязателен | По умолчанию | Описание |
|---|---|---|---|---| |---|---|---|---|---|
| `q` | string | yes | — | Поисковый запрос (1-100 символов) | | `q` | string | да | — | Поисковый запрос (1-100 символов) |
| `type` | enum | no | `all` | Фильтр: `all`, `share`, `bond` | | `type` | enum | нет | `all` | Фильтр: `all`, `share`, `bond` |
| `limit` | integer | no | `20` | Лимит результатов | | `limit` | integer | нет | `20` | Лимит результатов |
**Response:** **Response:**
```json ```json
@ -134,18 +134,18 @@
Скринер ценных бумаг по параметрам цены, объёма, доходности, дюрации, купона и срока погашения. Скринер ценных бумаг по параметрам цены, объёма, доходности, дюрации, купона и срока погашения.
**Parameters:** **Параметры:**
| Param | Type | Required | Description | | Параметр | Тип | Обязателен | Описание |
|---|---|---|---| |---|---|---|---|
| `type` | enum | yes | `share` или `bond` | | `type` | enum | да | `share` или `bond` |
| `priceMin`, `priceMax` | number | no | Диапазон цены | | `priceMin`, `priceMax` | number | нет | Диапазон цены |
| `volumeMin` | number | no | Минимальный объём | | `volumeMin` | number | нет | Минимальный объём |
| `yieldMin`, `yieldMax` | number | no | Диапазон доходности облигаций | | `yieldMin`, `yieldMax` | number | нет | Диапазон доходности облигаций |
| `durationMin`, `durationMax` | number | no | Диапазон дюрации | | `durationMin`, `durationMax` | number | нет | Диапазон дюрации |
| `sortBy` | string | no | Поле сортировки | | `sortBy` | string | нет | Поле сортировки |
| `sortOrder` | enum | no | `asc` или `desc` | | `sortOrder` | enum | нет | `asc` или `desc` |
| `page`, `pageSize` | integer | no | Пагинация | | `page`, `pageSize` | integer | нет | Пагинация |
**Response:** `{ data: { items, total, page, pageSize, totalPages }, meta }`. **Response:** `{ data: { items, total, page, pageSize, totalPages }, meta }`.
@ -155,7 +155,7 @@
Спецификация акции. Спецификация акции.
**Parameters:** `secid` — тикер (например, `SBER`) **Параметры:** `secid` — тикер (например, `SBER`)
**Response:** `ShareResponse` — спецификация + текущие рыночные данные. **Response:** `ShareResponse` — спецификация + текущие рыночные данные.
@ -216,7 +216,7 @@
Дивиденды акции. Дивиденды акции.
**Parameters:** `secid` — тикер **Параметры:** `secid` — тикер
**Response:** **Response:**
```json ```json
@ -236,12 +236,12 @@
Дневная история торгов акции. Дневная история торгов акции.
**Parameters:** **Параметры:**
| Param | Type | Required | Description | | Параметр | Тип | Обязателен | Описание |
|---|---|---|---| |---|---|---|---|
| `from` | string (date) | yes | Начальная дата (`YYYY-MM-DD`) | | `from` | string (date) | да | Начальная дата (`YYYY-MM-DD`) |
| `till` | string (date) | yes | Конечная дата (`YYYY-MM-DD`) | | `till` | string (date) | да | Конечная дата (`YYYY-MM-DD`) |
**Response:** **Response:**
```json ```json
@ -267,7 +267,7 @@
Спецификация облигации. Спецификация облигации.
**Parameters:** `secid` — тикер **Параметры:** `secid` — тикер
**Response:** `BondResponse` — спецификация + рыночные данные. **Response:** `BondResponse` — спецификация + рыночные данные.
@ -320,7 +320,7 @@
Дневная история торгов облигации. Дневная история торгов облигации.
**Parameters:** `from`, `till` (date) **Параметры:** `from`, `till` (date)
**Response:** **Response:**
```json ```json
@ -347,13 +347,13 @@
Свечи облигации. Свечи облигации.
**Parameters:** **Параметры:**
| Param | Type | Required | Description | | Параметр | Тип | Обязателен | Описание |
|---|---|---|---| |---|---|---|---|
| `interval` | enum | yes | `1h` или `24h` | | `interval` | enum | да | `1h` или `24h` |
| `from` | string (date) | yes | Начальная дата | | `from` | string (date) | да | Начальная дата |
| `till` | string (date) | yes | Конечная дата | | `till` | string (date) | да | Конечная дата |
**Response:** **Response:**
```json ```json
@ -374,11 +374,11 @@
} }
``` ```
## Portfolios ## Портфели
Все portfolio endpoints защищены JWT и возвращают envelope `{ data, meta }`. Все portfolio endpoints защищены JWT и возвращают envelope `{ data, meta }`.
| Endpoint | Method | Description | | Endpoint | Method | Описание |
|---|---|---| |---|---|---|
| `/api/v1/portfolios` | GET | Список портфелей пользователя | | `/api/v1/portfolios` | GET | Список портфелей пользователя |
| `/api/v1/portfolios` | POST | Создать портфель | | `/api/v1/portfolios` | POST | Создать портфель |

View File

@ -1,71 +1,71 @@
# Authentication & Authorization # Authentication и Authorization
## Architecture ## Архитектура
### Token-Based Authentication ### Token-based authentication
Система использует два типа JWT-токенов: Система использует два типа JWT-токенов:
| Token | Format | TTL | Storage | Purpose | | Token | Формат | TTL | Хранение | Назначение |
|-------|--------|-----|---------|---------| |-------|--------|-----|---------|---------|
| Access Token | JWT `{ sub, email, role }` | 15 min | Memory (React) + `Authorization: Bearer` | Authenticate API requests | | Access token | JWT `{ sub, email, role }` | 15 мин | Память React + `Authorization: Bearer` | Аутентифицирует API requests |
| Refresh Token | JWT `{ sub, jti }` | 7 days | httpOnly cookie + SHA-256 hash in DB | Issue new access tokens | | Refresh token | JWT `{ sub, jti }` | 7 дней | httpOnly cookie + SHA-256 hash в БД | Выпускает новые access tokens |
### Security ### Безопасность
- Passwords hashed with **bcrypt** (12 salt rounds) - Пароли хешируются через **bcrypt** (12 salt rounds)
- Refresh tokens stored as **bcrypt hash** in database - Refresh tokens хранятся в базе как **bcrypt hash**
- httpOnly, SameSite=Lax, Secure (production) cookies - Cookie: httpOnly, SameSite=Lax, Secure в production
- Access token never persisted to localStorage (XSS protection) - Access token не сохраняется в localStorage, чтобы снизить XSS-риск
- Auto-refresh on 401 with automatic retry of failed request - При 401 frontend автоматически обновляет token и повторяет исходный request
- Refresh token rotation: each refresh invalidates the old token - Refresh token rotation: каждый refresh инвалидирует старый token
### Flow ### Поток
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant User participant User as Пользователь
participant Frontend as React SPA participant Frontend as React SPA
participant Backend as NestJS API participant Backend as NestJS API
participant DB as SQLite (Prisma) participant DB as SQLite (Prisma)
User->>Frontend: Enter email & password User->>Frontend: Вводит email и password
Frontend->>Backend: POST /auth/login { email, password } Frontend->>Backend: POST /auth/login { email, password }
Backend->>DB: Find user by email Backend->>DB: Ищет пользователя по email
Backend->>Backend: bcrypt.compare(password, hash) Backend->>Backend: bcrypt.compare(password, hash)
Backend->>DB: Save refresh token hash Backend->>DB: Сохраняет refresh token hash
Backend-->>Frontend: { user, accessToken } + Set-Cookie (refreshToken) Backend-->>Frontend: { user, accessToken } + Set-Cookie (refreshToken)
Frontend->>Frontend: Store accessToken in memory Frontend->>Frontend: Хранит accessToken в памяти
Frontend-->>User: Redirect to app Frontend-->>User: Перенаправляет в приложение
Note over Frontend,Backend: Later API request Note over Frontend,Backend: Следующий API request
Frontend->>Backend: GET /auth/me (Authorization: Bearer <accessToken>) Frontend->>Backend: GET /auth/me (Authorization: Bearer <accessToken>)
Backend->>Backend: Verify JWT signature & expiry Backend->>Backend: Проверяет JWT signature и expiry
Backend-->>Frontend: { user } Backend-->>Frontend: { user }
Note over Frontend,Backend: Token refresh (auto on 401) Note over Frontend,Backend: Token refresh автоматически при 401
Frontend->>Backend: POST /auth/refresh (Cookie: refreshToken) Frontend->>Backend: POST /auth/refresh (Cookie: refreshToken)
Backend->>Backend: Verify refresh JWT Backend->>Backend: Проверяет refresh JWT
Backend->>DB: Compare refresh token hash Backend->>DB: Сравнивает refresh token hash
Backend->>DB: Rotate: save new refresh token hash Backend->>DB: Ротация: сохраняет новый refresh token hash
Backend-->>Frontend: { user, newAccessToken } + Set-Cookie (newRefreshToken) Backend-->>Frontend: { user, newAccessToken } + Set-Cookie (newRefreshToken)
Frontend->>Frontend: Update accessToken in memory Frontend->>Frontend: Обновляет accessToken в памяти
Frontend->>Backend: Retry original request Frontend->>Backend: Повторяет исходный request
Note over Frontend,Backend: Logout Note over Frontend,Backend: Logout
Frontend->>Backend: POST /auth/logout Frontend->>Backend: POST /auth/logout
Backend->>DB: Clear refresh token hash Backend->>DB: Очищает refresh token hash
Backend-->>Frontend: Clear cookie Backend-->>Frontend: Очищает cookie
Frontend->>Frontend: Clear accessToken Frontend->>Frontend: Очищает accessToken
``` ```
## API Endpoints ## API endpoints
All endpoints are under `/api/v1/auth`. Все endpoints находятся под `/api/v1/auth`.
### `POST /auth/register` ### `POST /auth/register`
Register a new user. Регистрирует нового пользователя.
**Request:** **Request:**
```json ```json
@ -76,11 +76,11 @@ Register a new user.
} }
``` ```
**Response:** `{ data: { user, accessToken }, meta }` + `Set-Cookie` with refresh token. **Response:** `{ data: { user, accessToken }, meta }` + `Set-Cookie` с refresh token.
### `POST /auth/login` ### `POST /auth/login`
Authenticate existing user. Аутентифицирует существующего пользователя.
**Request:** **Request:**
```json ```json
@ -94,19 +94,19 @@ Authenticate existing user.
### `POST /auth/refresh` ### `POST /auth/refresh`
Refresh access token. Reads refresh token from cookie. Обновляет access token. Refresh token читается из cookie.
**Response:** `{ data: { user, accessToken }, meta }` + new `Set-Cookie`. **Response:** `{ data: { user, accessToken }, meta }` + new `Set-Cookie`.
### `POST /auth/logout` ### `POST /auth/logout`
Invalidates refresh token. Requires `Authorization: Bearer`. Инвалидирует refresh token. Требует `Authorization: Bearer`.
**Response:** `{ data: { message }, meta }` + cookie cleared. **Response:** `{ data: { message }, meta }` + очищенная cookie.
### `GET /auth/me` ### `GET /auth/me`
Returns current user profile. Requires `Authorization: Bearer`. Возвращает профиль текущего пользователя. Требует `Authorization: Bearer`.
**Response:** **Response:**
```json ```json
@ -123,7 +123,7 @@ Returns current user profile. Requires `Authorization: Bearer`.
### `PATCH /auth/me` ### `PATCH /auth/me`
Update current user profile. Requires `Authorization: Bearer`. Обновляет профиль текущего пользователя. Требует `Authorization: Bearer`.
**Request:** **Request:**
```json ```json
@ -134,19 +134,20 @@ Update current user profile. Requires `Authorization: Bearer`.
## Authorization (RBAC) ## Authorization (RBAC)
| Role | Permissions | | Роль | Права |
|------|-------------| |------|-------------|
| `user` | View/edit own profile | | `user` | Просмотр и редактирование своего профиля |
| `admin` | All user permissions | | `admin` | Все права пользователя |
**Application level:** Global `JwtAuthGuard` protects all routes by default. Use `@Public()` decorator to bypass. `RolesGuard` checks required roles from `@Roles()` decorator. **Уровень приложения:** глобальный `JwtAuthGuard` по умолчанию защищает все routes.
Декоратор `@Public()` открывает публичные endpoints. `RolesGuard` проверяет роли из `@Roles()`.
## Environment Variables ## Переменные окружения
| Variable | Default | Description | | Переменная | По умолчанию | Описание |
|----------|---------|-------------| |----------|---------|-------------|
| `JWT_SECRET` | `dev-jwt-secret-...` | Secret for access token signing | | `JWT_SECRET` | `dev-jwt-secret-...` | Secret для подписи access token |
| `JWT_REFRESH_SECRET` | `dev-refresh-secret-...` | Secret for refresh token signing | | `JWT_REFRESH_SECRET` | `dev-refresh-secret-...` | Secret для подписи refresh token |
| `JWT_ACCESS_EXPIRES` | `15m` | Access token TTL | | `JWT_ACCESS_EXPIRES` | `15m` | Access token TTL |
| `JWT_REFRESH_EXPIRES` | `7d` | Refresh token TTL | | `JWT_REFRESH_EXPIRES` | `7d` | Refresh token TTL |
| `DATABASE_URL` | `file:./dev.db` | SQLite database URL (Prisma format) | | `DATABASE_URL` | `file:./dev.db` | URL SQLite в формате Prisma |

View File

@ -1,22 +1,22 @@
# Caching # Кеширование
## Cache Strategy ## Стратегия cache
In-memory кеш через `@nestjs/cache-manager` (cache-manager v5). Поддерживается миграция на Redis (см. ADR-002). In-memory кеш через `@nestjs/cache-manager` (cache-manager v5). Поддерживается миграция на Redis (см. ADR-002).
## Cache Flow ## Поток cache
```mermaid ```mermaid
flowchart LR flowchart LR
Request["Request Data"] Request["Запрос данных"]
CacheService["CacheService.getOrFetch()"] CacheService["CacheService.getOrFetch()"]
BuildKey["buildKey(prefix, keyParts)"] BuildKey["buildKey(prefix, keyParts)"]
CacheGet["cacheManager.get(key)"] CacheGet["cacheManager.get(key)"]
Hit["Cache HIT → return data"] Hit["Cache HIT → вернуть данные"]
Miss["Cache MISS"] Miss["Cache MISS"]
FetchFn["FetchFn() → get from MOEX"] FetchFn["fetchFn() → получить из MOEX"]
CacheSet["cacheManager.set(key, data, ttl)"] CacheSet["cacheManager.set(key, data, ttl)"]
Return["Return { data, fromCache, cachedAt }"] Return["Вернуть { data, fromCache, cachedAt }"]
Request --> CacheService Request --> CacheService
CacheService --> BuildKey CacheService --> BuildKey
@ -48,7 +48,7 @@ getOrFetch<T>(
- При cache hit возвращает `{ data, fromCache: true, cachedAt: null }` - При cache hit возвращает `{ data, fromCache: true, cachedAt: null }`
- При cache miss вызывает `fetchFn`, сохраняет результат с TTL и возвращает `{ data, fromCache: false, cachedAt: '...' }` - При cache miss вызывает `fetchFn`, сохраняет результат с TTL и возвращает `{ data, fromCache: false, cachedAt: '...' }`
## Cache Module Configuration ## Конфигурация CacheModule
`apps/backend/src/modules/cache/cache.module.ts`: `apps/backend/src/modules/cache/cache.module.ts`:
@ -68,13 +68,13 @@ getOrFetch<T>(
export class CacheModule {} export class CacheModule {}
``` ```
## Per-Data TTL ## TTL по типам данных
| Data Type | Config Key | Default TTL | Config Variable | | Тип данных | Config key | TTL по умолчанию | Переменная |
|---|---|---|---| |---|---|---|---|
| Market Data | `marketDataTtl` | 900s (15 min) | `CACHE_MARKET_DATA_TTL` | | Рыночные данные | `marketDataTtl` | 900s (15 мин) | `CACHE_MARKET_DATA_TTL` |
| History | `historyTtl` | 3600s (1h) | `CACHE_HISTORY_TTL` | | История | `historyTtl` | 3600s (1 ч) | `CACHE_HISTORY_TTL` |
| Candles | `candlesTtl` | 3600s (1h) | `CACHE_CANDLES_TTL` | | Свечи | `candlesTtl` | 3600s (1 ч) | `CACHE_CANDLES_TTL` |
| Security Spec | `securityTtl` | 86400s (24h) | `CACHE_SECURITY_TTL` | | Спецификация инструмента | `securityTtl` | 86400s (24 ч) | `CACHE_SECURITY_TTL` |
| Search | `searchTtl` | 3600s (1h) | `CACHE_SEARCH_TTL` | | Поиск | `searchTtl` | 3600s (1 ч) | `CACHE_SEARCH_TTL` |
| Dividends | `dividendsTtl` | 86400s (24h) | `CACHE_DIVIDENDS_TTL` | | Дивиденды | `dividendsTtl` | 86400s (24 ч) | `CACHE_DIVIDENDS_TTL` |

View File

@ -1,10 +1,10 @@
# Configuration # Конфигурация
Конфигурация загружается через `@nestjs/config` из `config/configuration.ts` и env-переменных. Конфигурация загружается через `@nestjs/config` из `config/configuration.ts` и env-переменных.
## Environment Variables ## Переменные окружения
| Variable | Default | Description | | Переменная | По умолчанию | Описание |
|---|---|---| |---|---|---|
| `PORT` | `3000` | Порт HTTP-сервера | | `PORT` | `3000` | Порт HTTP-сервера |
| `MOEX_BASE_URL` | `https://iss.moex.com/iss` | Базовый URL MOEX ISS API | | `MOEX_BASE_URL` | `https://iss.moex.com/iss` | Базовый URL MOEX ISS API |
@ -18,7 +18,7 @@
| `CACHE_SEARCH_TTL` | `3600` | TTL результатов поиска (секунды) | | `CACHE_SEARCH_TTL` | `3600` | TTL результатов поиска (секунды) |
| `CACHE_DIVIDENDS_TTL` | `86400` | TTL дивидендов (секунды) | | `CACHE_DIVIDENDS_TTL` | `86400` | TTL дивидендов (секунды) |
## Configuration File ## Файл конфигурации
`apps/backend/src/config/configuration.ts`: `apps/backend/src/config/configuration.ts`:

View File

@ -1,63 +1,128 @@
# Database Schema # Схема базы данных
## Overview ## Обзор
Используется **SQLite** через **Prisma ORM 7**. База данных находится в `apps/backend/dev.db`. Используется **SQLite** через **Prisma ORM 7**. База данных находится в `apps/backend/dev.db`.
## Schema ## Схема
```prisma ```prisma
generator client { generator client {
provider = "prisma-client" provider = "prisma-client-js"
output = "../src/generated/prisma"
} }
datasource db { datasource db {
provider = "sqlite" provider = "sqlite"
} }
model Portfolio {
id Int @id @default(autoincrement())
userId Int
name String
description String?
currency String @default("RUB")
targets String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
positions Position[]
@@unique([userId, name])
}
model Position {
id Int @id @default(autoincrement())
portfolioId Int
secid String
type String @default("share")
quantity Int
buyPrice Float?
buyDate DateTime?
notes String?
tags String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
portfolio Portfolio @relation(fields: [portfolioId], references: [id], onDelete: Cascade)
@@unique([portfolioId, secid])
}
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
email String @unique email String @unique
password String password String
name String? name String?
role String @default("user") role String @default("user")
refreshToken String? refreshToken String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
portfolios Portfolio[]
} }
``` ```
## Tables ## Таблицы
### Users ### User
| Column | Type | Constraints | Description | | Колонка | Тип | Ограничения | Описание |
|--------|------|-------------|-------------| |--------|------|-------------|-------------|
| id | INTEGER | PK, AUTOINCREMENT | User ID | | id | INTEGER | PK, AUTOINCREMENT | ID пользователя |
| email | TEXT | UNIQUE, NOT NULL | Email address | | email | TEXT | UNIQUE, NOT NULL | Email |
| password | TEXT | NOT NULL | bcrypt hash of password | | password | TEXT | NOT NULL | bcrypt hash пароля |
| name | TEXT | NULLABLE | Display name | | name | TEXT | NULLABLE | Отображаемое имя |
| role | TEXT | NOT NULL, DEFAULT 'user' | Role for RBAC | | role | TEXT | NOT NULL, DEFAULT 'user' | Роль для RBAC |
| refreshToken | TEXT | NULLABLE | bcrypt hash of current refresh token | | refreshToken | TEXT | NULLABLE | bcrypt hash текущего refresh token |
| createdAt | DATETIME | NOT NULL | Timestamp | | createdAt | DATETIME | NOT NULL | Дата создания |
| updatedAt | DATETIME | NOT NULL | Auto-updated timestamp | | updatedAt | DATETIME | NOT NULL | Дата последнего обновления |
### Portfolio
| Колонка | Тип | Ограничения | Описание |
|--------|------|-------------|-------------|
| id | INTEGER | PK, AUTOINCREMENT | ID портфеля |
| userId | INTEGER | FK -> User, NOT NULL | Владелец портфеля |
| name | TEXT | NOT NULL, UNIQUE per user | Название портфеля |
| description | TEXT | NULLABLE | Описание |
| currency | TEXT | DEFAULT 'RUB' | Валюта портфеля |
| targets | TEXT | NULLABLE | Целевые доли или настройки в JSON-строке |
| createdAt | DATETIME | NOT NULL | Дата создания |
| updatedAt | DATETIME | NOT NULL | Дата последнего обновления |
### Position
| Колонка | Тип | Ограничения | Описание |
|--------|------|-------------|-------------|
| id | INTEGER | PK, AUTOINCREMENT | ID позиции |
| portfolioId | INTEGER | FK -> Portfolio, NOT NULL | Портфель |
| secid | TEXT | NOT NULL | MOEX security ID |
| type | TEXT | DEFAULT 'share' | `share` или `bond` |
| quantity | INTEGER | NOT NULL | Количество |
| buyPrice | REAL | NULLABLE | Цена покупки |
| buyDate | DATETIME | NULLABLE | Дата покупки |
| notes | TEXT | NULLABLE | Заметки |
| tags | TEXT | NULLABLE | Tags в JSON-строке |
| createdAt | DATETIME | NOT NULL | Дата создания |
| updatedAt | DATETIME | NOT NULL | Дата последнего обновления |
## Prisma Client ## Prisma Client
The Prisma client is generated to `apps/backend/src/generated/prisma/` and imported via `@/generated/prisma/client`. Prisma client импортируется из `@prisma/client`.
Key service: `PrismaService` (`src/modules/prisma/prisma.service.ts`) — extends `PrismaClient`, handles connection lifecycle (`onModuleInit`/`onModuleDestroy`). Registered as a global module. Ключевой сервис: `PrismaService` (`src/modules/prisma/prisma.service.ts`) расширяет
`PrismaClient`, управляет жизненным циклом подключения (`onModuleInit`/`onModuleDestroy`) и
регистрируется через глобальный модуль.
## Migrations ## Миграции
Migrations are stored in `apps/backend/prisma/migrations/`. To create a new migration: Миграции лежат в `apps/backend/prisma/migrations/`. Создать новую миграцию:
```bash ```bash
npx prisma migrate dev --name <description> npx prisma migrate dev --name <description>
``` ```
To apply migrations in production: Применить миграции в production:
```bash ```bash
npx prisma migrate deploy npx prisma migrate deploy

View File

@ -1,55 +1,58 @@
# Backend Modules # Модули backend
## Module Dependency Graph ## Граф зависимостей модулей
### Импорты `AppModule`
```mermaid ```mermaid
graph TD flowchart TB
AppModule --> ConfigModule AppModule["AppModule"]
AppModule --> CacheModule GlobalModules["Глобальные модули<br/>ConfigModule<br/>PrismaModule<br/>CacheModule<br/>MoexClientModule"]
AppModule --> MoexClientModule FeatureModules["Feature-модули<br/>HealthModule<br/>AuthModule<br/>PortfolioModule<br/>SecuritiesModule<br/>SharesModule<br/>BondsModule<br/>CandlesModule"]
AppModule --> PrismaModule
AppModule --> HealthModule
AppModule --> AuthModule
AppModule --> SecuritiesModule
AppModule --> SharesModule
AppModule --> BondsModule
AppModule --> CandlesModule
AppModule --> PortfolioModule
subgraph Global_Modules["Global Modules"] AppModule --> GlobalModules
PrismaModule AppModule --> FeatureModules
CacheModule
MoexClientModule
end
AuthModule --> PrismaService["PrismaService"]
PortfolioModule --> PrismaService
PortfolioModule --> MoexClientService
SharesModule --> CacheService["CacheService"]
BondsModule --> CacheService
SecuritiesModule --> CacheService
CandlesModule --> CacheService
SharesModule --> MoexClientService["MoexClientService"]
BondsModule --> MoexClientService
SecuritiesModule --> MoexClientService
CandlesModule --> MoexClientService
``` ```
## Module List ### Зависимости от сервисов
| Module | Global | Path | Description | ```mermaid
flowchart TB
AuthModule["AuthModule"]
PortfolioModule["PortfolioModule"]
MarketModules["SecuritiesModule<br/>SharesModule<br/>BondsModule<br/>CandlesModule"]
PrismaService["PrismaService"]
CacheService["CacheService"]
MoexClientService["MoexClientService"]
PrismaModule --> PrismaService
CacheModule --> CacheService
MoexClientModule --> MoexClientService
AuthModule --> PrismaService
PortfolioModule --> PrismaService
PortfolioModule --> CacheService
PortfolioModule --> MoexClientService
MarketModules --> CacheService
MarketModules --> MoexClientService
```
## Список модулей
| Модуль | Глобальный | Путь | Описание |
|---|---|---|---| |---|---|---|---|
| `PrismaModule` | Yes | `modules/prisma/` | Prisma client for SQLite | | `PrismaModule` | Да | `modules/prisma/` | Prisma client для SQLite |
| `CacheModule` | Yes | `modules/cache/` | In-memory cache (cache-manager) | | `CacheModule` | Да | `modules/cache/` | In-memory cache через cache-manager |
| `MoexClientModule` | Yes | `modules/moex-client/` | HTTP-клиент MOEX ISS | | `MoexClientModule` | Да | `modules/moex-client/` | HTTP-клиент MOEX ISS |
| `HealthModule` | No | `modules/health/` | Health check endpoint | | `HealthModule` | Нет | `modules/health/` | Health check endpoint |
| `AuthModule` | No | `modules/auth/` | JWT auth, refresh cookie, guards | | `AuthModule` | Нет | `modules/auth/` | JWT auth, refresh cookie, guards |
| `SecuritiesModule` | No | `modules/securities/` | Поиск инструментов | | `SecuritiesModule` | Нет | `modules/securities/` | Поиск инструментов |
| `SharesModule` | No | `modules/shares/` | Акции | | `SharesModule` | Нет | `modules/shares/` | Акции |
| `BondsModule` | No | `modules/bonds/` | Облигации | | `BondsModule` | Нет | `modules/bonds/` | Облигации |
| `CandlesModule` | No | `modules/candles/` | Свечи OHLCV | | `CandlesModule` | Нет | `modules/candles/` | Свечи OHLCV |
| `PortfolioModule` | No | `modules/portfolio/` | Пользовательские портфели и аналитика | | `PortfolioModule` | Нет | `modules/portfolio/` | Пользовательские портфели и аналитика |
### PrismaModule ### PrismaModule

View File

@ -1,10 +1,10 @@
# MOEX Client # MOEX Client
## Overview ## Обзор
`MoexClientService` (`apps/backend/src/modules/moex-client/moex-client.service.ts`) — HTTP-клиент для MOEX ISS API. `MoexClientService` (`apps/backend/src/modules/moex-client/moex-client.service.ts`) — HTTP-клиент для MOEX ISS API.
## Rate Limiting ## Rate limiting
Использует `p-queue`: Использует `p-queue`:
@ -17,7 +17,7 @@ this.queue = new PQueue({
Все запросы к MOEX проходят через очередь — не более `MOEX_RATE_LIMIT` запросов в секунду. Все запросы к MOEX проходят через очередь — не более `MOEX_RATE_LIMIT` запросов в секунду.
## Circuit Breaker ## Circuit breaker
Состояние: закрыт → открыт → полуоткрыт (через таймаут). Состояние: закрыт → открыт → полуоткрыт (через таймаут).
@ -30,7 +30,7 @@ private circuitErrorCount = 0;
- В открытом состоянии все запросы мгновенно падают с ошибкой `"Circuit breaker is open"` - В открытом состоянии все запросы мгновенно падают с ошибкой `"Circuit breaker is open"`
- Через `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` (30) автоматически сбрасывается - Через `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` (30) автоматически сбрасывается
## Request Method ## Метод request
```typescript ```typescript
private async request<T>(path: string, params?: Record<string, string>): Promise<T> private async request<T>(path: string, params?: Record<string, string>): Promise<T>
@ -40,7 +40,7 @@ private async request<T>(path: string, params?: Record<string, string>): Promise
- Устанавливает `iss.meta=off` (отключает метаданные) - Устанавливает `iss.meta=off` (отключает метаданные)
- Таймаут: 10s - Таймаут: 10s
## Response Parsing ## Разбор response
MOEX возвращает данные в табличном формате: MOEX возвращает данные в табличном формате:
@ -55,9 +55,9 @@ MOEX возвращает данные в табличном формате:
Метод `extractTable` преобразует это в массив объектов по колонкам. Метод `extractTable` преобразует это в массив объектов по колонкам.
## Available MOEX Methods ## Доступные методы MOEX
| Method | MOEX Path | Description | | Метод | MOEX path | Описание |
|---|---|---| |---|---|---|
| `searchSecurities` | `/securities?q=` | Поиск инструментов | | `searchSecurities` | `/securities?q=` | Поиск инструментов |
| `getSecurityDescription` | `/securities/{secid}` | Спецификация | | `getSecurityDescription` | `/securities/{secid}` | Спецификация |
@ -69,7 +69,7 @@ MOEX возвращает данные в табличном формате:
| `getHistory` | `/engines/stock/markets/shares/securities/{secid}` | История акций | | `getHistory` | `/engines/stock/markets/shares/securities/{secid}` | История акций |
| `getBondHistory` | `/engines/stock/markets/bonds/securities/{secid}` | История облигаций | | `getBondHistory` | `/engines/stock/markets/bonds/securities/{secid}` | История облигаций |
## MOEX ISS Types ## MOEX ISS types
Все MOEX-типы описаны в `apps/backend/src/modules/moex-client/moex-client.types.ts`: Все MOEX-типы описаны в `apps/backend/src/modules/moex-client/moex-client.types.ts`:

View File

@ -1,8 +1,8 @@
# Backend Overview # Обзор backend
Backend — NestJS-приложение, единственная точка доступа к MOEX ISS. Backend — NestJS-приложение, единственная точка доступа к MOEX ISS.
## Entry Point ## Точка входа
`apps/backend/src/main.ts` — bootstrap: `apps/backend/src/main.ts` — bootstrap:
@ -12,16 +12,16 @@ Backend — NestJS-приложение, единственная точка д
- JwtAuthGuard (глобально, `@Public()` для открытых эндпоинтов) - JwtAuthGuard (глобально, `@Public()` для открытых эндпоинтов)
- cookie-parser (для refresh token) - cookie-parser (для refresh token)
- CORS включён (`credentials: true`) - CORS включён (`credentials: true`)
- Порт из `PORT` env (default: 3000) - Порт из `PORT` env (по умолчанию: 3000)
## Root Module ## Корневой модуль
`apps/backend/src/app.module.ts` импортирует: `apps/backend/src/app.module.ts` импортирует:
- `ConfigModule.forRoot` (глобальный, из `config/configuration.ts`) - `ConfigModule.forRoot` (глобальный, из `config/configuration.ts`)
- Импортируются 8 feature-модулей (Prisma + Cache + MoexClient — глобальные) - Импортируются 8 feature-модулей (Prisma + Cache + MoexClient — глобальные)
## Source Layout ## Структура исходников
``` ```
apps/backend/src/ apps/backend/src/

View File

@ -1,19 +1,19 @@
# Portfolio Module # PortfolioModule
Portfolio module позволяет пользователям создавать и вести виртуальные инвестиционные портфели для `PortfolioModule` позволяет пользователям создавать и вести виртуальные инвестиционные портфели для
аналитики и отслеживания позиций. аналитики и отслеживания позиций.
## Overview ## Обзор
- **Backend:** `PortfolioModule` (`apps/backend/src/modules/portfolio/`) - **Backend:** `PortfolioModule` (`apps/backend/src/modules/portfolio/`)
- **Frontend:** Protected pages at `/portfolios` and `/portfolios/:id` - **Frontend:** защищённые страницы `/portfolios` и `/portfolios/:id`
- **Database:** `Portfolio` and `Position` models (Prisma + SQLite) - **База данных:** модели `Portfolio` и `Position` (Prisma + SQLite)
## API Endpoints ## API endpoints
Все endpoints требуют JWT authentication (`JwtAuthGuard`). Все endpoints требуют JWT authentication через `JwtAuthGuard`.
| Endpoint | Method | Description | | Endpoint | Method | Описание |
|---|---|---| |---|---|---|
| `/api/v1/portfolios` | GET | Список портфелей пользователя | | `/api/v1/portfolios` | GET | Список портфелей пользователя |
| `/api/v1/portfolios` | POST | Создать портфель | | `/api/v1/portfolios` | POST | Создать портфель |
@ -25,7 +25,7 @@ Portfolio module позволяет пользователям создават
| `/api/v1/portfolios/:id/positions/:positionId` | PATCH | Обновить позицию | | `/api/v1/portfolios/:id/positions/:positionId` | PATCH | Обновить позицию |
| `/api/v1/portfolios/:id/positions/:positionId` | DELETE | Удалить позицию | | `/api/v1/portfolios/:id/positions/:positionId` | DELETE | Удалить позицию |
## Domain Model ## Доменная модель
``` ```
Portfolio Portfolio
@ -50,56 +50,63 @@ Position
couponPeriod, bondType, offerDate couponPeriod, bondType, offerDate
``` ```
## Analytics and PnL ## Analytics и PnL
The backend calculates performance metrics for each position and the portfolio as a whole: Backend рассчитывает метрики доходности для каждой позиции и портфеля целиком:
- **Total Cost:** `buyPrice * quantity` (adjusted for bonds: `(buyPrice / 100) * faceValue * quantity`). - **Total cost:** `buyPrice * quantity`; для облигаций: `(buyPrice / 100) * faceValue * quantity`.
- **Unrealized PnL:** `currentValue - totalCost`. - **Unrealized PnL:** `currentValue - totalCost`.
- **PnL %:** `(PnL / totalCost) * 100`. - **PnL %:** `(PnL / totalCost) * 100`.
- **Weighted Yield:** Portfolio-wide average yield based on position weights. - **Weighted yield:** средняя доходность портфеля с учётом весов позиций.
Analytics are available via the `/analytics` endpoint or as a `summary` object in the `/portfolios/:id` detail response. Analytics доступны через endpoint `/analytics` или как объект `summary` в детальном response
`/portfolios/:id`.
## Security Type Detection ## Определение типа инструмента
When adding a position, the backend fetches `getSecurityDescription(secid)` from MOEX. If `desc.group === 'stock_bonds'`, the position type is set to `bond`; otherwise `share`. This determines which enrichment path is used on read. При добавлении позиции backend получает из MOEX `getSecurityDescription(secid)`. Если
`desc.group === 'stock_bonds'`, тип позиции становится `bond`; иначе используется `share`. От типа
зависит путь обогащения при чтении портфеля.
## Price Computation (Shares) ## Расчёт цены акций
For shares: `currentPrice = marketData.last`, `currentValue = price * quantity`, with `change` and `changePercent` from `lastChange` and `lastChangePrcnt`. Для акций: `currentPrice = marketData.last`, `currentValue = price * quantity`; `change` и
`changePercent` берутся из `lastChange` и `lastChangePrcnt`.
## Price Computation (Bonds) ## Расчёт цены облигаций
For bonds, MOEX returns prices as a percentage of face value (e.g., 98.5 = 98.5% of 1000 RUB). Для облигаций MOEX возвращает цены в процентах от номинала: например, 98.5 означает 98.5% от
1000 RUB.
- `currentPrice = marketData.last` (% of face value) - `currentPrice = marketData.last` (% of face value)
- `currentValue = (price / 100) * faceValue * quantity` - `currentValue = (price / 100) * faceValue * quantity`
Bond enrichment also fetches: Обогащение облигаций также получает:
| Field | Source | Description | | Поле | Источник | Описание |
|---|---|---| |---|---|---|
| `yieldToMaturity` | `getBondMarketData().yield` | YTM (%) | | `yieldToMaturity` | `getBondMarketData().yield` | YTM (%) |
| `duration` | `getBondMarketData().duration` | Modified duration (years) | | `duration` | `getBondMarketData().duration` | Modified duration в годах |
| `couponValue` | `getBondData().couponValue` | Coupon amount in RUB | | `couponValue` | `getBondData().couponValue` | Размер купона в RUB |
| `couponPercent` | `getBondData().couponPercent` | Coupon rate (%) | | `couponPercent` | `getBondData().couponPercent` | Ставка купона (%) |
| `couponPeriod` | `getBondData().couponPeriod` | Days between payments | | `couponPeriod` | `getBondData().couponPeriod` | Дней между выплатами |
| `nextCouponDate` | `getBondData().nextCoupon` | Next coupon date | | `nextCouponDate` | `getBondData().nextCoupon` | Дата следующего купона |
| `matDate` | `getBondData().matDate` | Maturity date | | `matDate` | `getBondData().matDate` | Дата погашения |
| `offerDate` | `getBondData().offerDate` | Early redemption date | | `offerDate` | `getBondData().offerDate` | Дата досрочного погашения |
| `accruedInt` | `getBondData().accruedInt` | Accrued interest per bond (RUB) | | `accruedInt` | `getBondData().accruedInt` | НКД на облигацию (RUB) |
| `bondType` | `getBondData().bondType` | "ОФЗ", "Корпоративная", etc. | | `bondType` | `getBondData().bondType` | "ОФЗ", "Корпоративная", etc. |
| `bid` / `offer` | `getBondMarketData()` | Current bid/ask (% of face) | | `bid` / `offer` | `getBondMarketData()` | Текущий bid/ask в % от номинала |
## MOEX Board Selection ## Выбор MOEX board
The backend uses the following default MOEX boards: Backend использует следующие MOEX boards по умолчанию:
- **Shares:** `TQBR` (Т+: Акции — безадрес.) - **Акции:** `TQBR` (Т+: Акции — безадрес.)
- **Bonds:** `TQCB` (Т+: Корпоративные облигации — безадрес.) - **Облигации:** `TQCB` (Т+: Корпоративные облигации — безадрес.)
Some bonds (primarily OFZ government bonds) trade on `TQOB` board. If the requested board has no trading data, the backend falls back to any board with a non-null `LAST` price. Data is cached with standard market data TTL (900s). Некоторые облигации, в первую очередь ОФЗ, торгуются на board `TQOB`. Если на запрошенном board нет
торговых данных, backend выбирает любой board с ненулевой ценой `LAST`. Данные кешируются со
стандартным market data TTL (900s).
## Future Phases ## Будущие этапы
1. **Positions v2** — pie chart, filtering 1. **Positions v2** — pie chart, filtering
2. **Transactions** — buy/sell history, average cost basis 2. **Transactions** — buy/sell history, average cost basis

View File

@ -1,41 +1,43 @@
# Securities Module # SecuritiesModule
The Securities module provides search and filtering capabilities for MOEX financial instruments. `SecuritiesModule` отвечает за поиск и фильтрацию финансовых инструментов MOEX.
## Overview ## Обзор
- **Backend:** `SecuritiesModule` (`apps/backend/src/modules/securities/`) - **Backend:** `SecuritiesModule` (`apps/backend/src/modules/securities/`)
- **Frontend:** Search bar in layout, dedicated Screener page at `/screener` - **Frontend:** поисковая строка в layout и отдельная страница Screener по `/screener`
- **Source:** MOEX ISS API - **Источник:** MOEX ISS API
## Search API ## Search API
`GET /api/v1/securities/search?q={query}&type={type}&limit={limit}` `GET /api/v1/securities/search?q={query}&type={type}&limit={limit}`
Searches for securities by ticker or name. Ищет ценные бумаги по тикеру или названию.
- **type:** `all` (default), `share`, `bond`. - **type:** `all` по умолчанию, `share`, `bond`.
- **Results:** List of `SearchResultItem` with `secid`, `isin`, `shortName`, `type`, `listLevel`. - **Результат:** список `SearchResultItem` с `secid`, `isin`, `shortName`, `type`, `listLevel`.
## Security Screener ## Security Screener
`GET /api/v1/securities/screener` `GET /api/v1/securities/screener`
Allows filtering the entire MOEX board (shares or bonds) by market parameters. Фильтрует весь board MOEX для акций или облигаций по рыночным параметрам.
### Query Parameters ### Query parameters
| Parameter | Type | Description | | Параметр | Тип | Описание |
|---|---|---| |---|---|---|
| `type` | `share` \| `bond` | **Required.** Board type to scan. | | `type` | `share` \| `bond` | **Обязателен.** Тип board для сканирования. |
| `priceMin` / `Max` | number | Price range (RUB for shares, % for bonds) | | `priceMin` / `Max` | number | Диапазон цены: RUB для акций, проценты для облигаций |
| `volumeMin` | number | Minimum daily trading volume | | `volumeMin` | number | Минимальный дневной объём торгов |
| `yieldMin` / `Max` | number | YTM range (Bonds only) | | `yieldMin` / `Max` | number | Диапазон YTM, только для облигаций |
| `durationMin` / `Max`| number | Duration range in years (Bonds only) | | `durationMin` / `Max`| number | Диапазон duration в годах, только для облигаций |
| `changePercentMin` | number | Minimum daily price change % (Shares only) | | `changePercentMin` | number | Минимальное дневное изменение цены в %, только для акций |
| `sortBy` | string | Field to sort by (`price`, `volume`, `yieldToMaturity`, etc.) | | `sortBy` | string | Поле сортировки: `price`, `volume`, `yieldToMaturity`, etc. |
| `sortOrder` | `asc` \| `desc` | Sort direction | | `sortOrder` | `asc` \| `desc` | Направление сортировки |
| `page` / `pageSize` | number | Pagination parameters | | `page` / `pageSize` | number | Параметры пагинации |
### Implementation Details ### Детали реализации
The Screener fetches the full market board from MOEX in a single batch request, caches it for 60 seconds (standard market data TTL), and applies filtering/sorting/pagination on the backend. This ensures high performance for complex queries without overwhelming the MOEX API. Screener получает полный market board из MOEX одним batch request, кеширует данные на 60 секунд
или стандартный market data TTL и применяет фильтрацию, сортировку и пагинацию на backend. Так
сложные запросы остаются быстрыми и не перегружают MOEX API.

View File

@ -1,10 +1,10 @@
# Code Generation # Генерация кода
## OpenAPI Types ## OpenAPI types
Frontend генерирует TypeScript-типы из Swagger-спецификации бэкенда. Frontend генерирует TypeScript-типы из Swagger-спецификации бэкенда.
### Generate Types ### Сгенерировать типы
```bash ```bash
npm run codegen -w apps/frontend npm run codegen -w apps/frontend
@ -16,17 +16,17 @@ npm run codegen -w apps/frontend
openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts
``` ```
### Prerequisites ### Требования
- Backend должен быть запущен на `localhost:3000` - Backend должен быть запущен на `localhost:3000`
- Swagger UI доступен на `http://localhost:3000/api/docs` - Swagger UI доступен на `http://localhost:3000/api/docs`
### Output ### Результат
- `apps/frontend/src/api/types.ts` — сгенерированные типы `paths` и `operations` - `apps/frontend/src/api/types.ts` — сгенерированные типы `paths` и `operations`
- Live Swagger JSON на `http://localhost:3000/api/docs-json` остаётся источником OpenAPI-контракта - Live Swagger JSON на `http://localhost:3000/api/docs-json` остаётся источником OpenAPI-контракта
### Verify Artifacts ### Проверка артефактов
После регенерации OpenAPI artifacts запустите: После регенерации OpenAPI artifacts запустите:
@ -37,6 +37,6 @@ npm run test -w apps/backend -- src/openapi-artifacts.spec.ts
Тест проверяет, что checked-in frontend types содержат актуальные auth, screener и portfolio paths Тест проверяет, что checked-in frontend types содержат актуальные auth, screener и portfolio paths
и не содержат локальный alternate port. и не содержат локальный alternate port.
### Manual Types ### Ручные типы
Помимо codegen, используются рукописные типы в `apps/frontend/src/api/responses.ts`. Они не полностью соответствуют codegen'овым и поддерживаются вручную. Помимо codegen, используются рукописные типы в `apps/frontend/src/api/responses.ts`. Они не полностью соответствуют codegen'овым и поддерживаются вручную.

View File

@ -1,8 +1,8 @@
# Commands # Команды
## Root Workspace ## Корневой workspace
| Command | Description | | Команда | Описание |
|---|---| |---|---|
| `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 | | `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 |
| `npm run dev:frontend` | Vite dev-сервер на `:5173`, проксирует `/api` на backend | | `npm run dev:frontend` | Vite dev-сервер на `:5173`, проксирует `/api` на backend |
@ -16,9 +16,9 @@
| `npm run format` | Prettier для всех `*.{ts,tsx}` | | `npm run format` | Prettier для всех `*.{ts,tsx}` |
| `npm run format:check` | Проверка Prettier для всех `*.{ts,tsx}` | | `npm run format:check` | Проверка Prettier для всех `*.{ts,tsx}` |
## Backend Workspace ## Backend workspace
| Command | Description | | Команда | Описание |
|---|---| |---|---|
| `npm run build -w apps/backend` | `nest build` | | `npm run build -w apps/backend` | `nest build` |
| `npm run start:dev -w apps/backend` | `nest start --watch` | | `npm run start:dev -w apps/backend` | `nest start --watch` |
@ -28,9 +28,9 @@
| `npm run test:watch -w apps/backend` | `vitest` | | `npm run test:watch -w apps/backend` | `vitest` |
| `npm run test:integration -w apps/backend` | Opt-in live MOEX integration tests, требуется network access | | `npm run test:integration -w apps/backend` | Opt-in live MOEX integration tests, требуется network access |
## Frontend Workspace ## Frontend workspace
| Command | Description | | Команда | Описание |
|---|---| |---|---|
| `npm run dev -w apps/frontend` | `vite` | | `npm run dev -w apps/frontend` | `vite` |
| `npm run build -w apps/frontend` | `tsc -b && vite build` | | `npm run build -w apps/frontend` | `tsc -b && vite build` |
@ -39,15 +39,15 @@
| `npm run lint -w apps/frontend` | ESLint для `src/**/*.{ts,tsx}` | | `npm run lint -w apps/frontend` | ESLint для `src/**/*.{ts,tsx}` |
| `npm run test -w apps/frontend` | Frontend Vitest suite | | `npm run test -w apps/frontend` | Frontend Vitest suite |
## Docs Workspace ## Docs workspace
| Command | Description | | Команда | Описание |
|---|---| |---|---|
| `npm run dev -w apps/docs` | Docusaurus dev-server | | `npm run dev -w apps/docs` | Docusaurus dev-server |
| `npm run build -w apps/docs` | Production build документации | | `npm run build -w apps/docs` | Production build документации |
| `npm run serve -w apps/docs` | Локальная проверка production build | | `npm run serve -w apps/docs` | Локальная проверка production build |
## Single Test ## Один тест
```bash ```bash
npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend

View File

@ -1,6 +1,6 @@
# Code Conventions # Соглашения по коду
## Formatting ## Форматирование
Prettier (`.prettierrc`): Prettier (`.prettierrc`):
@ -26,17 +26,17 @@ ESLint запускается для backend (`apps/backend`) и frontend (`apps
Запуск: `npm run lint` Запуск: `npm run lint`
## Naming (backend) ## Именование в backend
- ПаскальКейс для модулей, контроллеров, сервисов - ПаскальКейс для модулей, контроллеров, сервисов
- `const` для всех переменных (кроме случаев, где нужна мутация) - `const` для всех переменных (кроме случаев, где нужна мутация)
- DTO-файлы в `dto/` внутри каждого модуля - DTO-файлы в `dto/` внутри каждого модуля
## Imports (backend) ## Imports в backend
Алиас: `@/*``apps/backend/src/*` Алиас: `@/*``apps/backend/src/*`
## Imports (frontend) ## Imports в frontend
Алиас: `@/*``apps/frontend/src/*` Алиас: `@/*``apps/frontend/src/*`
@ -50,7 +50,7 @@ ESLint запускается для backend (`apps/backend`) и frontend (`apps
- Strict: true - Strict: true
- ESModule interop: true - ESModule interop: true
## Backend-specific ## Специфика backend
- Использует SWC через `unplugin-swc` (vitest config) - Использует SWC через `unplugin-swc` (vitest config)
- Контроллеры содержат только маршрутизацию и вызовы сервисов - Контроллеры содержат только маршрутизацию и вызовы сервисов

View File

@ -1,12 +1,12 @@
# Testing # Тестирование
## Backend Tests ## Тесты backend
Фреймворк: **vitest** (с `unplugin-swc` для быстрой компиляции, не ts-jest). Фреймворк: **vitest** (с `unplugin-swc` для быстрой компиляции, не ts-jest).
Конфигурация vitest в `apps/backend/` использует SWC для трансформации TypeScript. Конфигурация vitest в `apps/backend/` использует SWC для трансформации TypeScript.
### Run All Tests ### Запустить все тесты
```bash ```bash
npm run test:backend npm run test:backend
@ -14,21 +14,21 @@ npm run test:backend
npm run test -w apps/backend npm run test -w apps/backend
``` ```
### Run Single Test ### Запустить один тест
```bash ```bash
npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend
``` ```
### Watch Mode ### Watch mode
```bash ```bash
npm run test:watch -w apps/backend npm run test:watch -w apps/backend
``` ```
## Test Files ## Тестовые файлы
| File | What it tests | | Файл | Что проверяет |
|---|---| |---|---|
| `shares.service.spec.ts` | SharesService | | `shares.service.spec.ts` | SharesService |
| `bonds.service.spec.ts` | BondsService | | `bonds.service.spec.ts` | BondsService |
@ -37,7 +37,7 @@ npm run test:watch -w apps/backend
| `securities.service.spec.ts` | SecuritiesService | | `securities.service.spec.ts` | SecuritiesService |
| `moex-client.service.spec.ts` | MoexClientService | | `moex-client.service.spec.ts` | MoexClientService |
## Frontend Tests ## Тесты frontend
Фреймворк: **Vitest 4** + **React Testing Library** + **MSW**. Фреймворк: **Vitest 4** + **React Testing Library** + **MSW**.
@ -51,7 +51,7 @@ npm run test -w apps/frontend
Тесты покрывают API-клиент, auth context, hooks, базовые pages и shared components. Тесты покрывают API-клиент, auth context, hooks, базовые pages и shared components.
## Live MOEX Integration Tests ## Live MOEX integration tests
Live MOEX checks вынесены из default backend suite. Live MOEX checks вынесены из default backend suite.

View File

@ -1,6 +1,6 @@
# API Client # API client
## Client (`api/client.ts`) ## Клиент (`api/client.ts`)
Использует нативный `fetch` с единой обёрткой `request<T>`. Использует нативный `fetch` с единой обёрткой `request<T>`.
@ -16,7 +16,7 @@ async function request<T>(
- Парсит JSON-ответ в `ApiEnvelope<{ data: T, meta: ApiResponseMeta }>` - Парсит JSON-ответ в `ApiEnvelope<{ data: T, meta: ApiResponseMeta }>`
- Выбрасывает `Error` при HTTP-ошибке - Выбрасывает `Error` при HTTP-ошибке
## API Functions ## API functions
| Function | Method | Path | | Function | Method | Path |
|---|---|---| |---|---|---|
@ -48,7 +48,7 @@ async function request<T>(
| `removePosition(portfolioId, positionId)` | DELETE | `/api/v1/portfolios/:id/positions/:positionId` | | `removePosition(portfolioId, positionId)` | DELETE | `/api/v1/portfolios/:id/positions/:positionId` |
| `getPortfolioAnalytics(portfolioId)` | GET | `/api/v1/portfolios/:id/analytics` | | `getPortfolioAnalytics(portfolioId)` | GET | `/api/v1/portfolios/:id/analytics` |
## Types ## Типы
Ручные типы ответов в `api/responses.ts`: Ручные типы ответов в `api/responses.ts`:

View File

@ -1,4 +1,4 @@
# Components # Компоненты
## Layout (`components/Layout.tsx`) ## Layout (`components/Layout.tsx`)

View File

@ -1,8 +1,8 @@
# Hooks # Хуки
Все хуки используют TanStack Query v5. Все хуки используют TanStack Query v5.
| Hook | Query Key | Stale Time | Description | | Хук | Query key | Stale time | Описание |
|---|---|---|---| |---|---|---|---|
| `useSearch(query)` | `['securities', 'search', query]` | 60s | Поиск инструментов (enabled: query ≥ 2 символов) | | `useSearch(query)` | `['securities', 'search', query]` | 60s | Поиск инструментов (enabled: query ≥ 2 символов) |
| `useStock(secid)` | `['stock', secid]` | 900s | Спецификация акции | | `useStock(secid)` | `['stock', secid]` | 900s | Спецификация акции |
@ -11,7 +11,7 @@
| `useBond(secid)` | `['bond', secid]` | 900s | Спецификация облигации | | `useBond(secid)` | `['bond', secid]` | 900s | Спецификация облигации |
| `useBondCandles(secid, interval, from, till)` | `['bondCandles', secid, interval, from, till]` | 3600s | Свечи облигации | | `useBondCandles(secid, interval, from, till)` | `['bondCandles', secid, interval, from, till]` | 3600s | Свечи облигации |
## Query Configuration ## Конфигурация Query
```typescript ```typescript
const queryClient = new QueryClient({ const queryClient = new QueryClient({
@ -25,7 +25,7 @@ const queryClient = new QueryClient({
}); });
``` ```
## Hook Pattern ## Паттерн hook
Каждый хук: Каждый хук:

View File

@ -1,8 +1,8 @@
# Frontend Overview # Обзор frontend
React SPA, собранная с Vite. React SPA, собранная с Vite.
## Tech Stack ## Технологический стек
- React 18 - React 18
- react-router-dom v6 - react-router-dom v6
@ -12,11 +12,11 @@ React SPA, собранная с Vite.
- Vitest + Testing Library + MSW - Vitest + Testing Library + MSW
- Vite 5 - Vite 5
## Source Layout ## Структура исходников
``` ```
apps/frontend/src/ apps/frontend/src/
├── main.tsx # Entry point ├── main.tsx # Точка входа
├── App.tsx # BrowserRouter ├── App.tsx # BrowserRouter
├── routes.tsx # Маршруты ├── routes.tsx # Маршруты
├── api/ ├── api/
@ -65,6 +65,6 @@ apps/frontend/src/
└── styles.css └── styles.css
``` ```
## Entry Point ## Точка входа
`main.tsx` рендерит `App.tsx`, который содержит `BrowserRouter``AppRoutes`. `main.tsx` рендерит `App.tsx`, который содержит `BrowserRouter``AppRoutes`.

View File

@ -1,8 +1,8 @@
# Routes # Маршруты
Определены в `apps/frontend/src/routes.tsx`. Определены в `apps/frontend/src/routes.tsx`.
| Path | Component | Access | Description | | Path | Component | Доступ | Описание |
|---|---|---|---| |---|---|---|---|
| `/` | `HomePage` | Public | Главная страница | | `/` | `HomePage` | Public | Главная страница |
| `/stocks/:secid` | `StockPage` | Public | Страница акции | | `/stocks/:secid` | `StockPage` | Public | Страница акции |
@ -20,7 +20,7 @@
- `<main>` с максимальной шириной 1200px - `<main>` с максимальной шириной 1200px
- `<Outlet />` для контента страницы - `<Outlet />` для контента страницы
## Route Structure ## Структура маршрутов
```tsx ```tsx
<Routes> <Routes>

View File

@ -1,10 +1,10 @@
# Styling # Стили
## Approach ## Подход
CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind. CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind.
## CSS Custom Properties ## CSS custom properties
Определены в `styles.css`: Определены в `styles.css`:

View File

@ -1,24 +1,24 @@
# Getting Started # Быстрый старт
## Prerequisites ## Требования
- Node.js 20+ - Node.js 20+
- npm 9+ - npm 9+
## Quick Start ## Запуск для разработки
```bash ```bash
# 1. Clone repository # 1. Клонировать репозиторий
git clone <repo-url> git clone <repo-url>
cd moex-vibe cd moex-vibe
# 2. Install dependencies # 2. Установить зависимости
npm install npm install
# 3. Start backend (http://localhost:3000) # 3. Запустить backend (http://localhost:3000)
npm run dev:backend npm run dev:backend
# 4. In another terminal — start frontend (http://localhost:5173) # 4. В другом терминале запустить frontend (http://localhost:5173)
npm run dev:frontend npm run dev:frontend
``` ```
@ -34,9 +34,9 @@ docker compose up --build
- Backend: http://localhost:3000 - Backend: http://localhost:3000
- Swagger UI: http://localhost:3000/api/docs - Swagger UI: http://localhost:3000/api/docs
## Verify ## Проверка
Open http://localhost:5173 (or http://localhost:80 for Docker). You should see: Откройте http://localhost:5173 или http://localhost:80 при запуске через Docker. Должны быть доступны:
1. Страница с поисковой строкой и заголовком "MoexVibe" 1. Страница с поисковой строкой и заголовком "MoexVibe"
2. Swagger UI на http://localhost:3000/api/docs со всеми endpoint'ами 2. Swagger UI на http://localhost:3000/api/docs со всеми endpoint'ами

View File

@ -1,6 +1,6 @@
# CI/CD # CI/CD
## CI Pipeline ## CI pipeline
Определён в `.gitea/workflows/ci.yml`. Запускается на push/PR в `main`. Определён в `.gitea/workflows/ci.yml`. Запускается на push/PR в `main`.

View File

@ -1,10 +1,10 @@
# Docker # Docker
## Architecture ## Архитектура
```mermaid ```mermaid
graph TD graph TD
User["User"] User["Пользователь"]
Nginx["Nginx :80"] Nginx["Nginx :80"]
Backend["Node.js :3000"] Backend["Node.js :3000"]
MOEX["MOEX ISS<br/>iss.moex.com"] MOEX["MOEX ISS<br/>iss.moex.com"]
@ -15,7 +15,7 @@ graph TD
Backend -->|"API calls"| MOEX Backend -->|"API calls"| MOEX
``` ```
## Services ## Сервисы
### Backend (`Dockerfile.backend`) ### Backend (`Dockerfile.backend`)
@ -57,7 +57,7 @@ CMD ["nginx", "-g", "daemon off;"]
Собирает статику, затем раздаёт через Nginx. Собирает статику, затем раздаёт через Nginx.
### Nginx Config (`nginx.conf`) ### Конфигурация Nginx (`nginx.conf`)
```nginx ```nginx
server { server {
@ -105,7 +105,7 @@ services:
- backend - backend
``` ```
## Run ## Запуск
```bash ```bash
docker compose up --build docker compose up --build

View File

@ -6,21 +6,21 @@ slug: /
Веб-приложение для анализа ценных бумаг Московской биржи (MOEX). Веб-приложение для анализа ценных бумаг Московской биржи (MOEX).
## Tech Stack ## Технологический стек
- **Backend:** NestJS, TypeScript, OpenAPI (Swagger) - **Backend:** NestJS, TypeScript, OpenAPI (Swagger)
- **Frontend:** React 18, TypeScript, Vite, TanStack Query v5, lightweight-charts v4 - **Frontend:** React 18, TypeScript, Vite, TanStack Query v5, lightweight-charts v4
- **Infrastructure:** Docker, docker-compose - **Инфраструктура:** Docker, docker-compose
- **CI:** Gitea Actions - **CI:** Gitea Actions
## Repository Structure ## Структура репозитория
``` ```
moex-vibe/ moex-vibe/
├── apps/ ├── apps/
│ ├── backend/ # NestJS API (единственная точка доступа к MOEX) │ ├── backend/ # NestJS API (единственная точка доступа к MOEX)
│ ├── frontend/ # React SPA │ ├── frontend/ # React SPA
│ └── docs/ # Docusaurus documentation site │ └── docs/ # Docusaurus-сайт документации
├── docs/ ├── docs/
│ └── superpowers/ │ └── superpowers/
│ └── specs/ # Согласованные SDD-спецификации │ └── specs/ # Согласованные SDD-спецификации
@ -32,7 +32,7 @@ moex-vibe/
└── tsconfig.base.json └── tsconfig.base.json
``` ```
## Key Principles ## Ключевые принципы
- Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую. - Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую.
- npm workspaces монорепозиторий: `apps/backend`, `apps/frontend` и `apps/docs`. - npm workspaces монорепозиторий: `apps/backend`, `apps/frontend` и `apps/docs`.

View File

@ -4,7 +4,7 @@ import type * as Preset from '@docusaurus/preset-classic';
const config: Config = { const config: Config = {
title: 'MoexVibe', title: 'MoexVibe',
tagline: 'Analysis of MOEX securities', tagline: 'Анализ ценных бумаг MOEX',
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
url: 'https://moexvibe.example.com', url: 'https://moexvibe.example.com',

View File

@ -35,12 +35,12 @@ const sidebars: SidebarsConfig = {
}, },
{ {
type: 'category', type: 'category',
label: 'Infrastructure', label: 'Инфраструктура',
items: ['infrastructure/docker', 'infrastructure/ci'], items: ['infrastructure/docker', 'infrastructure/ci'],
}, },
{ {
type: 'category', type: 'category',
label: 'Development', label: 'Разработка',
items: [ items: [
'development/commands', 'development/commands',
'development/testing', 'development/testing',
@ -50,7 +50,7 @@ const sidebars: SidebarsConfig = {
}, },
{ {
type: 'category', type: 'category',
label: 'Architecture Decisions (ADR)', label: 'Архитектурные решения (ADR)',
items: [ items: [
'adr/index', 'adr/index',
'adr/ADR-001-backend-single-point-of-access', 'adr/ADR-001-backend-single-point-of-access',

View File

@ -0,0 +1,212 @@
# Русификация документации и исправление архитектурных схем — SDD-спецификация
**Дата:** 2026-06-15
**Статус:** черновик для ревью
## Контекст
Документация проекта публикуется только из `apps/docs` через Docusaurus. Текущая структура уже
зафиксирована в `docs/superpowers/specs/2026-06-13-docusaurus-docs-design.md`: страницы лежат в
`apps/docs/docs`, навигация описана в `apps/docs/sidebars.ts`, Mermaid включён через
`@docusaurus/theme-mermaid`.
Сейчас большая часть человекочитаемого текста в опубликованной документации написана на английском:
заголовки, подписи таблиц, описания endpoint'ов, ADR summary, названия разделов sidebar. Проект
ведётся на русском, поэтому документация должна быть русскоязычной, оставляя на английском только
технические названия, идентификаторы и общепринятые инженерные термины.
На странице `apps/docs/docs/architecture.md` первая Mermaid-схема `System Architecture` смешивает
в одном `graph TD` внешний контур приложения и внутреннее устройство backend. Из-за этого Docusaurus
рендерит слишком широкую и высокую схему: блоки и подписи стрелок визуально накладываются друг на
друга, особенно вокруг `Backend (NestJS)`, `Browser (React SPA)`, `NestJS API`, cache, MOEX и SQLite.
## Цели
1. Перевести опубликованную человекочитаемую документацию в `apps/docs` на русский язык.
2. Оставить без перевода технические названия и идентификаторы, которые должны совпадать с кодом,
API, библиотеками или файлами.
3. Исправить читаемость Mermaid-схем в `apps/docs/docs/architecture.md`, убрав наложение блоков и
подписей.
4. Сохранить текущую структуру Docusaurus и URL/slugs, чтобы не ломать существующие ссылки.
5. Задать проверяемые критерии приёмки для последующей реализации.
## Не цели
- Не менять backend, frontend, OpenAPI contract, runtime-конфигурацию или Docker-инфраструктуру.
- Не переименовывать файлы документации, ADR-файлы, route slugs и ссылки между страницами, если это
не требуется для исправления битой ссылки.
- Не внедрять i18n с несколькими локалями: сайт остаётся одноязычным с `defaultLocale: 'ru'`.
- Не переписывать архитектурные решения по сути. ADR переводятся как документация, но их смысл,
статус и последствия сохраняются.
- Не менять визуальную тему Docusaurus сверх минимальной поддержки читаемых Mermaid-схем.
## Область изменений
### Опубликованная документация
Перевод и редактура затрагивают:
- `apps/docs/docusaurus.config.ts`
- `apps/docs/sidebars.ts`
- `apps/docs/docs/intro.md`
- `apps/docs/docs/getting-started.md`
- `apps/docs/docs/architecture.md`
- `apps/docs/docs/backend/*.md`
- `apps/docs/docs/frontend/*.md`
- `apps/docs/docs/development/*.md`
- `apps/docs/docs/infrastructure/*.md`
- `apps/docs/docs/adr/*.md`
### Стили
`apps/docs/src/css/custom.css` можно менять только если после разбиения Mermaid-схем остаются
проблемы с шириной, переносами или горизонтальной прокруткой. CSS не должен быть основным способом
исправления сломанной схемы.
## Правила русификации
### Переводить
- Заголовки страниц и разделов: `Architecture` -> `Архитектура`, `Getting Started` -> `Быстрый старт`.
- Sidebar labels: `Infrastructure` -> `Инфраструктура`, `Development` -> `Разработка`,
`Architecture Decisions (ADR)` -> `Архитектурные решения (ADR)`.
- Описания таблиц и колонок: `Description` -> `Описание`, `Required` -> `Обязателен`,
`Default` -> `По умолчанию`.
- Поясняющий текст, инструкции, списки, summaries, примечания к flow/sequence diagrams.
- Подписи Mermaid-стрелок, если они не являются точным API, cookie/header или именем метода:
`fetches` -> `запрашивает`, `uses` -> `использует`, `raw data` -> `сырые данные`.
- Ошибочные или неактуальные формулировки, найденные при переводе, если их можно исправить по
текущему коду или уже существующим SDD/ADR.
### Не переводить
- Названия технологий и библиотек: `NestJS`, `React`, `Vite`, `TanStack Query`, `Prisma`,
`SQLite`, `Docusaurus`, `Docker`, `Nginx`, `Vitest`, `MSW`, `Swagger`, `OpenAPI`,
`lightweight-charts`, `openapi-fetch`.
- Архитектурные технические существительные, уже используемые в проекте как термины: `backend`, `frontend`,
`middleware`, `endpoint`, `request`, `response`, `cache`, `cookie`, `access token`,
`refresh token`, `rate limiter`, `circuit breaker`, `codegen`, `workspace`, `build`, `lint`.
- Имена модулей, классов, DTO, методов, env vars, scripts, файлов и директорий:
`AuthModule`, `MoexClientService`, `CACHE_MARKET_DATA_TTL`, `npm run dev:backend`,
`apps/backend/src/modules/auth`.
- HTTP methods, URL paths, JSON keys, TypeScript/Prisma identifiers, enum values и code blocks.
- ADR file names and IDs: `ADR-001-backend-single-point-of-access.md`, `ADR-010`, etc.
### Стиль русского текста
- Писать для разработчика проекта, без маркетингового тона.
- Использовать короткие предложения и конкретные глаголы.
- Сохранять технические термины в одном написании по всему сайту.
- Не переводить термин, если перевод ухудшает связь с кодом или общепринятой практикой.
## План исправления `architecture.md`
Текущую первую схему нужно заменить набором меньших схем:
1. `Архитектура системы` — внешний контур: Browser/React SPA, NestJS API, SQLite, cache,
MOEX ISS API. Направление лучше сделать `flowchart LR`, чтобы схема читалась слева направо.
2. `Внутренние модули backend` — отдельная схема только для NestJS feature/global modules:
AuthModule, SharesModule, BondsModule, CandlesModule, SecuritiesModule, PortfolioModule,
HealthModule, MoexClientModule, CacheModule/CacheService, PrismaModule/PrismaService.
3. `Поток авторизованного запроса` — существующий `sequenceDiagram` оставить как отдельный
сценарий, но перевести человекочитаемые подписи и сверить путь запроса с актуальными docs/API.
Для первой схемы недопустимо помещать большой `subgraph Backend_Internal` внутрь графа внешней
архитектуры. Если нужно показать, что `NestJS API` состоит из модулей, это должно быть ссылкой
текстом или отдельной диаграммой ниже.
## Рекомендуемый Mermaid-паттерн
```mermaid
flowchart LR
Browser["Browser<br/>(React SPA)"]
API["NestJS API<br/>:3000"]
Cache["In-memory cache<br/>(cache-manager)"]
MOEX["MOEX ISS API<br/>iss.moex.com"]
DB[("SQLite<br/>(Prisma)")]
Browser -->|"/api/v1/*"| API
Browser -->|"Authorization: Bearer"| API
Browser -->|"Cookie: refreshToken"| API
API -->|"getOrFetch()"| Cache
API -->|"GET /iss/*.json"| MOEX
API -->|"Prisma ORM"| DB
Cache -->|"данные"| API
MOEX -->|"сырые данные"| API
DB -->|"пользователи и портфели"| API
```
В реализации можно менять конкретное расположение узлов, если итоговая схема проходит визуальную
проверку и остаётся семантически эквивалентной.
## План реализации
1. Создать отдельную ветку `codex/russian-docs-architecture-diagrams`, если текущая ветка не
предназначена для этой работы.
2. Пройти по navigation/config files:
`apps/docs/docusaurus.config.ts` и `apps/docs/sidebars.ts`.
3. Перевести overview-страницы:
`intro.md`, `getting-started.md`, `architecture.md`.
4. В `architecture.md` заменить первую Mermaid-схему на две небольшие схемы и перевести
человекочитаемые подписи в `sequenceDiagram`.
5. Перевести backend-раздел, сохраняя имена модулей, endpoints, DTO, env vars и code examples.
6. Перевести frontend-раздел, сохраняя названия React/Vite/TanStack Query и имена hooks/components.
7. Перевести infrastructure/development-разделы, сохраняя команды, scripts, имена workflow jobs,
Dockerfile paths и package names.
8. Перевести ADR index и ADR pages без изменения сути решений, статусов и ссылок.
9. Проверить Markdown-ссылки, таблицы и fenced code blocks.
10. Запустить `npm run build:docs`.
11. Если build проходит, открыть локальный Docusaurus и визуально проверить `architecture.md`:
нет наложения блоков, подписи стрелок читаемы, схема не уезжает за viewport на desktop.
## Критерии приёмки
- Все опубликованные страницы в `apps/docs/docs/**/*.md` имеют русские заголовки и русский
человекочитаемый текст, кроме разрешённых технических названий.
- `apps/docs/sidebars.ts` показывает русские labels для всех человекочитаемых категорий.
- `apps/docs/docusaurus.config.ts` содержит русскую tagline или нейтральную русскоязычную фразу.
- Имена файлов, route slugs, ADR IDs, endpoint paths, env vars, code blocks и API examples не
переименованы ради перевода.
- `apps/docs/docs/architecture.md` не содержит одной большой Mermaid-схемы, которая одновременно
показывает внешний контур и внутренние backend modules.
- Mermaid-схемы на странице `architecture.md` рендерятся без наложения узлов и подписей.
- `npm run build:docs` завершается успешно.
- После сборки нет новых broken links или broken Markdown links сверх текущей политики
`onBrokenLinks: 'warn'`, `onBrokenMarkdownLinks: 'warn'`.
## Проверка качества
### Автоматическая
```bash
npm run build:docs
```
Ожидаемый результат: Docusaurus build завершается без ошибки.
### Ручная
1. Запустить `npm run dev:docs`.
2. Открыть страницу `/architecture`.
3. Проверить, что первая схема помещается в контентную область на desktop без наложения.
4. Проверить, что sequence diagram читается и подписи на русском там, где они не являются
техническими идентификаторами.
5. Выборочно открыть по одной странице из разделов Backend, Frontend, Инфраструктура, Разработка и
ADR, чтобы подтвердить единый стиль перевода.
## Риски и решения
| Риск | Решение |
|---|---|
| Чрезмерный перевод ломает связь с кодом | Следовать списку "Не переводить" и оставлять code/API terms как есть |
| Mermaid всё ещё строит слишком широкую схему | Делить диаграмму ещё мельче, а не пытаться лечить layout только CSS |
| ADR после перевода могут звучать как новые решения | Сохранять ID, статус, контекст, consequences и не менять смысл |
| Перевод таблиц может сломать Markdown | После каждой группы страниц запускать build или локально просматривать diff |
## Решение для ревью
Рекомендуемый путь: сначала выполнить русификацию и разбиение схем без изменения информационной
архитектуры сайта. После этого отдельно оценить, нужно ли делать полноценную редакторскую
нормализацию терминов или добавлять глоссарий. Для текущей задачи глоссарий в отдельной странице
не нужен: достаточно единых правил в этой спецификации и аккуратного применения по docs.