docs: translate docs to russian
All checks were successful
CI / lint (pull_request) Successful in 2m8s
CI / test (pull_request) Successful in 1m57s
CI / build (pull_request) Successful in 2m5s
CI / lint (push) Successful in 2m1s
CI / test (push) Successful in 1m51s
CI / build (push) Successful in 2m9s

This commit is contained in:
Sergey Krylov 2026-06-16 05:13:34 +03:00
parent 739405a597
commit 106467e5c4
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
**Date:** 2026-06-13
**Deciders:** Architect, Tech Lead
**Статус:** Accepted
**Дата:** 2026-06-13
**Участники решения:** Architect, Tech Lead
## Context
## Контекст
Frontend должен отображать данные Московской биржи. MOEX ISS API отдаёт сырые данные со сложной структурой (вложенные таблицы, различные форматы). Прямые запросы с фронта приведут к дублированию логики нормализации, усложнят обработку ошибок и сделают систему зависимой от внешнего API.
## Decision
## Решение
Backend (NestJS) является единственной точкой доступа к MOEX. Frontend никогда не обращается к MOEX напрямую.
Backend:
@ -17,7 +17,7 @@ Backend:
- Обрабатывает ошибки MOEX (пустые данные, rate limit, таймауты)
- Предоставляет собственный OpenAPI-контракт для фронта
## Consequences
## Последствия
- Единый источник правды для трансформации данных
- Изоляция изменений MOEX API — меняется только MoexClient
- Централизованное кеширование сокращает количество запросов к 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
**Date:** 2026-06-13
**Deciders:** Architect, Tech Lead
**Статус:** Accepted
**Дата:** 2026-06-13
**Участники решения:** Architect, Tech Lead
## Context
## Контекст
Для MVP требуется кеширование MOEX-данных, чтобы снизить нагрузку на внешнее API и обеспечить приемлемое время ответа. На начальном этапе нет требований к горизонтальному масштабированию, и хочется избежать внешних зависимостей.
## Decision
## Решение
Использовать `@nestjs/cache-manager` с MemoryStore. TTL настраивается per-endpoint через конфигурацию.
Архитектура позволяет переключиться на Redis заменой импорта провайдера:
@ -26,7 +26,7 @@ CacheModule.registerAsync({
})
```
## Consequences
## Последствия
- Нет внешних зависимостей для MVP
- Кеш сбрасывается при рестарте сервера (приемлемо для read-only приложения)
- Чистый путь миграции на Redis

View File

@ -1,20 +1,20 @@
# ADR-003: Rate Limiting Strategy for MOEX Client
# ADR-003: Стратегия rate limiting для MOEX client
**Status:** Accepted
**Date:** 2026-06-13
**Deciders:** Architect, Tech Lead
**Статус:** Accepted
**Дата:** 2026-06-13
**Участники решения:** Architect, Tech Lead
## Context
## Контекст
MOEX ISS не документирует жёсткие лимиты на количество запросов, но массовые запросы могут привести к блокировке или ухудшению качества обслуживания. Backend является единственным клиентом MOEX и должен контролировать исходящий трафик.
## Decision
## Решение
Внедрить два механизма в 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 секунд. После таймаута — пробный запрос для восстановления.
## Consequences
## Последствия
- Плавная нагрузка на MOEX, без пиков
- Автоматическое восстановление после сбоев MOEX
- Graceful degradation: при отключённом circuit breaker возвращаются кешированные данные

View File

@ -1,16 +1,16 @@
# ADR-004: Feature Modules by Domain
# ADR-004: Feature-модули по доменам
**Status:** Accepted
**Date:** 2026-06-13
**Deciders:** Architect, Tech Lead
**Статус:** Accepted
**Дата:** 2026-06-13
**Участники решения:** Architect, Tech Lead
## Context
## Контекст
NestJS рекомендует модульную архитектуру. Требования указывают на архитектуру по feature modules. Модули должны иметь чёткие границы и быть тестируемыми изолированно.
## Decision
## Решение
Каждый бизнес-домен — отдельный NestJS feature module:
| Module | Responsibility |
| Модуль | Ответственность |
|--------|---------------|
| `MoexClientModule` | HTTP-клиент к MOEX ISS, rate limiting, circuit breaker |
| `CacheModule` | Абстракция кеширования |
@ -22,7 +22,7 @@ NestJS рекомендует модульную архитектуру. Тре
Каждый 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
**Date:** 2026-06-13
**Deciders:** Architect, Tech Lead
**Статус:** Accepted
**Дата:** 2026-06-13
**Участники решения:** Architect, Tech Lead
## Context
## Контекст
Frontend должен потреблять API бэкенда. Ручное написание клиентов и DTO приводит к рассинхронизации с бэкендом и ошибкам типизации.
## Decision
## Решение
Использовать `openapi-typescript` + `openapi-fetch` для генерации:
- TypeScript типов (DTO, request/response schemas)
@ -33,7 +33,7 @@ export function useStock(secid: string) {
}
```
## Consequences
## Последствия
- Полная типобезопасность на стыке frontend/backend
- Автоматическая синхронизация с API-контрактом
- 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
**Date:** 2026-06-13
**Deciders:** Architect, Product
**Статус:** Accepted
**Дата:** 2026-06-13
**Участники решения:** Architect, Product
## Context
## Контекст
MOEX предоставляет корпоративную информацию (CCI) — финансовую отчётность по МСФО/РСБУ. Данные включают отчёты о прибылях/убытках, балансовые отчёты, мультипликаторы. Однако:
- CCI API имеет собственную сложную структуру (виды отчётности, периоды, индикаторы)
- Данные требуют дополнительной нормализации и расчёта метрик
- Для MVP пользователи хотят базовую информацию (цена, купон, график)
## Decision
## Решение
Не включать CCI в MVP. Roadmap на post-MVP.
## Consequences
## Последствия
- Меньший объём работы в MVP
- API не привязывается к CCI-схемам (будет отдельный модуль)
- Пользователи не увидят мультипликаторы (P/E, EV/EBITDA) в первой версии

View File

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

View File

@ -3,13 +3,13 @@ sidebar_position: 14
title: ADR-008
---
# ADR-008: Authentication & Authorization System
# ADR-008: Система Authentication и Authorization
## Status
## Статус
Accepted
## Context
## Контекст
Приложение MoexVibe публичное, но для персонализации и будущих функций (избранное, уведомления, подписки) требуется система аутентификации. Необходимо решение, которое:
@ -18,26 +18,26 @@ Accepted
- Позволяет расширять ролевую модель
- Интегрируется в существующий стек (NestJS + React)
## Decision
## Решение
### Backend
- **Database:** SQLite через Prisma ORM (без необходимости внешнего сервиса)
- **Auth strategy:** JWT access tokens (15m) + refresh tokens (7d) in httpOnly cookies
- **Password storage:** bcrypt with 12 salt rounds
- **Guard model:** Global `JwtAuthGuard` (все маршруты защищены по умолчанию, `@Public()` для открытых)
- **База данных:** SQLite через Prisma ORM без внешнего сервиса
- **Auth strategy:** JWT access tokens (15m) + refresh tokens (7d) в httpOnly cookies
- **Хранение паролей:** bcrypt with 12 salt rounds
- **Guard model:** глобальный `JwtAuthGuard` (все маршруты защищены по умолчанию, `@Public()` для открытых)
- **RBAC:** Роли `user` и `admin`, проверка через `RolesGuard`
### Frontend
- **State management:** React Context (`AuthContext`) для хранения пользователя и access token
- **Token storage:** Access token только в памяти (не localStorage), refresh token в httpOnly cookie
- **Управление состоянием:** React Context (`AuthContext`) для хранения пользователя и access token
- **Token storage:** access token только в памяти (не localStorage), refresh token в httpOnly cookie
- **Auto-refresh:** При 401 → автоматический вызов `/auth/refresh` → повтор оригинального запроса
- **Route protection:** `ProtectedRoute` компонент, редирект на `/login` с return URL
- **Route protection:** компонент `ProtectedRoute`, редирект на `/login` с return URL
## Consequences
## Последствия
### Positive
### Плюсы
- httpOnly cookie защищает refresh token от XSS
- Access token в памяти защищён от кражи через localStorage
@ -45,17 +45,17 @@ Accepted
- Refresh token rotation повышает безопасность
- Глобальный guard — безопасность по умолчанию
### Negative
### Минусы
- Access token теряется при полной перезагрузке страницы (восстанавливается через refresh cookie)
- Для production требуется генерация надёжных JWT_SECRET
- Нет rate limiting на login endpoint (TODO)
- Rate limiting на login endpoint не входит в текущее решение
- Нет верификации email (принятое упрощение)
## Migration Path
## Путь миграции
Для перехода на PostgreSQL потребуется:
1. Изменить `provider` в schema.prisma на `postgresql`
2. Обновить `DATABASE_URL`
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
- Portfolio and Position have clear relational structure (1:N)
- JSON fields cover semi-structured requirements (targets, tags) without adding another database
- No need for complex queries across tags at current scale (50 positions max per portfolio)
- Straightforward migration to PostgreSQL if needed
- SQLite уже используется для модели User
- Portfolio и Position имеют понятную реляционную структуру 1:N
- JSON-поля закрывают полуструктурированные требования (`targets`, `tags`) без новой базы
- На текущем масштабе не нужны сложные запросы по tags (до 50 позиций на портфель)
- При необходимости есть прямой путь миграции на PostgreSQL
## Consequences
## Последствия
- JSON fields not individually queryable in SQLite (acceptable for Phase 1)
- Targets edited as full JSON replacement (not atomic per-item update)
- Cascade delete handles portfolio → position cleanup
- JSON-поля не индексируются и не запрашиваются по отдельности в SQLite, что приемлемо для Phase 1
- Targets редактируются полной заменой JSON, без атомарного обновления отдельных элементов
- 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
- Backend caching reduces MOEX API calls
- Avoids N individual price requests from frontend
- Единый источник правды для финансовых расчётов
- Frontend получает готовые к отображению данные
- Backend cache сокращает количество запросов к MOEX API
- Frontend не делает N отдельных запросов цен
## Consequences
## Последствия
- Backend makes N MOEX requests per portfolio read (cached by TTL 900s)
- Portfolio endpoint cannot use naive response caching (prices per-user)
- Extra load on backend when many users view portfolios simultaneously
- Backend делает N запросов к MOEX при чтении портфеля; они кешируются с TTL 900s
- Portfolio endpoint не может использовать наивное response caching, потому что цены считаются для пользователя
- При одновременном просмотре портфелей многими пользователями растёт нагрузка на 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-002](ADR-002-in-memory-cache) | Accepted | In-Memory Cache Strategy |
| [ADR-003](ADR-003-rate-limiting-strategy) | Accepted | Rate Limiting Strategy |
| [ADR-004](ADR-004-feature-modules) | Accepted | Feature Modules Architecture |
| [ADR-005](ADR-005-openapi-codegen-frontend) | Accepted | OpenAPI Codegen for Frontend |
| [ADR-006](ADR-006-no-cci) | Deprecated | No Custom Components Infrastructure |
| [ADR-007](ADR-007-two-level-caching) | Draft | Two-Level Caching (In-Memory + Redis) |
| [ADR-008](ADR-008-auth-system) | Accepted | Authentication & Authorization |
| [ADR-009](ADR-009-portfolio-domain) | Accepted | Portfolio Domain Model |
| [ADR-010](ADR-010-backend-price-computation) | Accepted | Backend Price Computation |
| [ADR-001](ADR-001-backend-single-point-of-access) | Accepted | Backend — единственная точка доступа к MOEX |
| [ADR-002](ADR-002-in-memory-cache) | Accepted | Стратегия in-memory cache |
| [ADR-003](ADR-003-rate-limiting-strategy) | Accepted | Стратегия rate limiting |
| [ADR-004](ADR-004-feature-modules) | Accepted | Архитектура feature-модулей |
| [ADR-005](ADR-005-openapi-codegen-frontend) | Accepted | OpenAPI codegen для frontend |
| [ADR-006](ADR-006-no-cci) | Deprecated | CCI вынесен за пределы MVP |
| [ADR-007](ADR-007-two-level-caching) | Draft | Двухуровневый cache: backend + frontend |
| [ADR-008](ADR-008-auth-system) | Accepted | Authentication и Authorization |
| [ADR-009](ADR-009-portfolio-domain) | Accepted | Доменная модель портфеля |
| [ADR-010](ADR-010-backend-price-computation) | Accepted | Расчёт цен на backend |
Все опубликованные ADR находятся в `apps/docs/docs/adr/` и отображаются в этом Docusaurus-разделе.

View File

@ -1,94 +1,101 @@
# Architecture
# Архитектура
## System Architecture
## Архитектура системы
```mermaid
graph TD
flowchart LR
Browser["Browser<br/>(React SPA)"]
Backend["NestJS API<br/>:3000"]
Cache["In-Memory Cache<br/>(cache-manager)"]
API["NestJS API<br/>:3000"]
Cache["In-memory cache<br/>(cache-manager)"]
MOEX["MOEX ISS API<br/>iss.moex.com"]
DB[("SQLite Database<br/>(Prisma)")]
DB[("SQLite<br/>(Prisma)")]
Browser -->|"/api/v1/*"| Backend
Browser -->|"Cookie: refreshToken"| Backend
Browser -->|"Authorization: Bearer"| Backend
Backend -->|"getOrFetch()"| Cache
Backend -->|"GET /iss/*.json"| MOEX
Backend -->|"Prisma ORM"| DB
Cache -->|"data"| Backend
DB -->|"users"| Backend
MOEX -->|"raw data"| Backend
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
Browser -->|"/api/v1/*"| API
Browser -->|"Cookie: refreshToken"| API
Browser -->|"Authorization: Bearer"| API
API -->|"getOrFetch()"| Cache
API -->|"GET /iss/*.json"| MOEX
API -->|"Prisma ORM"| DB
Cache -->|"данные"| API
DB -->|"пользователи и портфели"| API
MOEX -->|"сырые данные"| API
```
## 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
sequenceDiagram
participant User
participant User as Пользователь
participant Frontend as React SPA
participant Backend as NestJS API
participant Cache as In-Memory Cache
participant Cache as In-memory cache
participant MOEX as MOEX ISS
participant AuthDB as SQLite
User->>Frontend: View stock SBER
Note over Frontend: Access token in memory
User->>Frontend: Открывает акцию SBER
Note over Frontend: access token хранится в памяти
Frontend->>Backend: GET /securities/shares/SBER (Authorization: Bearer)
Backend->>Backend: JwtAuthGuard validates token
Backend->>Backend: JwtAuthGuard проверяет token
Backend->>Cache: getOrFetch('share:SBER')
alt Cache miss
alt cache miss
Cache->>Backend: null
Backend->>MOEX: GET /iss/.../SBER.json
MOEX-->>Backend: raw data
MOEX-->>Backend: сырые данные
Backend->>Cache: set('share:SBER', ..., TTL=900)
else Cache hit
else cache hit
Cache-->>Backend: cached data
end
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-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-004](adr/ADR-004-feature-modules) | Feature modules architecture |
| [ADR-005](adr/ADR-005-openapi-codegen-frontend) | OpenAPI codegen for frontend |
| [ADR-006](adr/ADR-006-no-cci) | No CCI (Custom Components Infrastructure) |
| [ADR-007](adr/ADR-007-two-level-caching) | Two-level caching strategy |
| [ADR-008](adr/ADR-008-auth-system) | Authentication & Authorization |
| [ADR-009](adr/ADR-009-portfolio-domain) | Portfolio domain model |
| [ADR-010](adr/ADR-010-backend-price-computation) | Backend price computation |
| [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-003](adr/ADR-003-rate-limiting-strategy) | Rate limiting через p-queue |
| [ADR-004](adr/ADR-004-feature-modules) | Архитектура feature-модулей |
| [ADR-005](adr/ADR-005-openapi-codegen-frontend) | OpenAPI codegen для frontend |
| [ADR-006](adr/ADR-006-no-cci) | Отказ от CCI (Custom Components Infrastructure) |
| [ADR-007](adr/ADR-007-two-level-caching) | Двухуровневая стратегия cache |
| [ADR-008](adr/ADR-008-auth-system) | Authentication и Authorization |
| [ADR-009](adr/ADR-009-portfolio-domain) | Доменная модель портфеля |
| [ADR-010](adr/ADR-010-backend-price-computation) | Расчёт цен на backend |
## Response Format
## Формат ответа
Все ответы 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`
- HttpExceptionFilter (catch-all)
- TransformInterceptor (авто-обёртка в `ApiResponse`)

View File

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

View File

@ -1,71 +1,71 @@
# Authentication & Authorization
# Authentication и Authorization
## Architecture
## Архитектура
### Token-Based Authentication
### Token-based authentication
Система использует два типа JWT-токенов:
| Token | Format | TTL | Storage | Purpose |
| Token | Формат | TTL | Хранение | Назначение |
|-------|--------|-----|---------|---------|
| Access Token | JWT `{ sub, email, role }` | 15 min | Memory (React) + `Authorization: Bearer` | Authenticate API requests |
| Refresh Token | JWT `{ sub, jti }` | 7 days | httpOnly cookie + SHA-256 hash in DB | Issue new access tokens |
| Access token | JWT `{ sub, email, role }` | 15 мин | Память React + `Authorization: Bearer` | Аутентифицирует API requests |
| Refresh token | JWT `{ sub, jti }` | 7 дней | httpOnly cookie + SHA-256 hash в БД | Выпускает новые access tokens |
### Security
### Безопасность
- Passwords hashed with **bcrypt** (12 salt rounds)
- Refresh tokens stored as **bcrypt hash** in database
- httpOnly, SameSite=Lax, Secure (production) cookies
- Access token never persisted to localStorage (XSS protection)
- Auto-refresh on 401 with automatic retry of failed request
- Refresh token rotation: each refresh invalidates the old token
- Пароли хешируются через **bcrypt** (12 salt rounds)
- Refresh tokens хранятся в базе как **bcrypt hash**
- Cookie: httpOnly, SameSite=Lax, Secure в production
- Access token не сохраняется в localStorage, чтобы снизить XSS-риск
- При 401 frontend автоматически обновляет token и повторяет исходный request
- Refresh token rotation: каждый refresh инвалидирует старый token
### Flow
### Поток
```mermaid
sequenceDiagram
participant User
participant User as Пользователь
participant Frontend as React SPA
participant Backend as NestJS API
participant DB as SQLite (Prisma)
User->>Frontend: Enter email & password
User->>Frontend: Вводит 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->>DB: Save refresh token hash
Backend->>DB: Сохраняет refresh token hash
Backend-->>Frontend: { user, accessToken } + Set-Cookie (refreshToken)
Frontend->>Frontend: Store accessToken in memory
Frontend-->>User: Redirect to app
Frontend->>Frontend: Хранит accessToken в памяти
Frontend-->>User: Перенаправляет в приложение
Note over Frontend,Backend: Later API request
Note over Frontend,Backend: Следующий API request
Frontend->>Backend: GET /auth/me (Authorization: Bearer <accessToken>)
Backend->>Backend: Verify JWT signature & expiry
Backend->>Backend: Проверяет JWT signature и expiry
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)
Backend->>Backend: Verify refresh JWT
Backend->>DB: Compare refresh token hash
Backend->>DB: Rotate: save new refresh token hash
Backend->>Backend: Проверяет refresh JWT
Backend->>DB: Сравнивает refresh token hash
Backend->>DB: Ротация: сохраняет новый refresh token hash
Backend-->>Frontend: { user, newAccessToken } + Set-Cookie (newRefreshToken)
Frontend->>Frontend: Update accessToken in memory
Frontend->>Backend: Retry original request
Frontend->>Frontend: Обновляет accessToken в памяти
Frontend->>Backend: Повторяет исходный request
Note over Frontend,Backend: Logout
Frontend->>Backend: POST /auth/logout
Backend->>DB: Clear refresh token hash
Backend-->>Frontend: Clear cookie
Frontend->>Frontend: Clear accessToken
Backend->>DB: Очищает refresh token hash
Backend-->>Frontend: Очищает cookie
Frontend->>Frontend: Очищает accessToken
```
## API Endpoints
## API endpoints
All endpoints are under `/api/v1/auth`.
Все endpoints находятся под `/api/v1/auth`.
### `POST /auth/register`
Register a new user.
Регистрирует нового пользователя.
**Request:**
```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`
Authenticate existing user.
Аутентифицирует существующего пользователя.
**Request:**
```json
@ -94,19 +94,19 @@ Authenticate existing user.
### `POST /auth/refresh`
Refresh access token. Reads refresh token from cookie.
Обновляет access token. Refresh token читается из cookie.
**Response:** `{ data: { user, accessToken }, meta }` + new `Set-Cookie`.
### `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`
Returns current user profile. Requires `Authorization: Bearer`.
Возвращает профиль текущего пользователя. Требует `Authorization: Bearer`.
**Response:**
```json
@ -123,7 +123,7 @@ Returns current user profile. Requires `Authorization: Bearer`.
### `PATCH /auth/me`
Update current user profile. Requires `Authorization: Bearer`.
Обновляет профиль текущего пользователя. Требует `Authorization: Bearer`.
**Request:**
```json
@ -134,19 +134,20 @@ Update current user profile. Requires `Authorization: Bearer`.
## Authorization (RBAC)
| Role | Permissions |
| Роль | Права |
|------|-------------|
| `user` | View/edit own profile |
| `admin` | All user permissions |
| `user` | Просмотр и редактирование своего профиля |
| `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_REFRESH_SECRET` | `dev-refresh-secret-...` | Secret for refresh token signing |
| `JWT_SECRET` | `dev-jwt-secret-...` | Secret для подписи access token |
| `JWT_REFRESH_SECRET` | `dev-refresh-secret-...` | Secret для подписи refresh token |
| `JWT_ACCESS_EXPIRES` | `15m` | Access 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).
## Cache Flow
## Поток cache
```mermaid
flowchart LR
Request["Request Data"]
Request["Запрос данных"]
CacheService["CacheService.getOrFetch()"]
BuildKey["buildKey(prefix, keyParts)"]
CacheGet["cacheManager.get(key)"]
Hit["Cache HIT → return data"]
Hit["Cache HIT → вернуть данные"]
Miss["Cache MISS"]
FetchFn["FetchFn() → get from MOEX"]
FetchFn["fetchFn() → получить из MOEX"]
CacheSet["cacheManager.set(key, data, ttl)"]
Return["Return { data, fromCache, cachedAt }"]
Return["Вернуть { data, fromCache, cachedAt }"]
Request --> CacheService
CacheService --> BuildKey
@ -48,7 +48,7 @@ getOrFetch<T>(
- При cache hit возвращает `{ data, fromCache: true, cachedAt: null }`
- При cache miss вызывает `fetchFn`, сохраняет результат с TTL и возвращает `{ data, fromCache: false, cachedAt: '...' }`
## Cache Module Configuration
## Конфигурация CacheModule
`apps/backend/src/modules/cache/cache.module.ts`:
@ -68,13 +68,13 @@ getOrFetch<T>(
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` |
| History | `historyTtl` | 3600s (1h) | `CACHE_HISTORY_TTL` |
| Candles | `candlesTtl` | 3600s (1h) | `CACHE_CANDLES_TTL` |
| Security Spec | `securityTtl` | 86400s (24h) | `CACHE_SECURITY_TTL` |
| Search | `searchTtl` | 3600s (1h) | `CACHE_SEARCH_TTL` |
| Dividends | `dividendsTtl` | 86400s (24h) | `CACHE_DIVIDENDS_TTL` |
| Рыночные данные | `marketDataTtl` | 900s (15 мин) | `CACHE_MARKET_DATA_TTL` |
| История | `historyTtl` | 3600s (1 ч) | `CACHE_HISTORY_TTL` |
| Свечи | `candlesTtl` | 3600s (1 ч) | `CACHE_CANDLES_TTL` |
| Спецификация инструмента | `securityTtl` | 86400s (24 ч) | `CACHE_SECURITY_TTL` |
| Поиск | `searchTtl` | 3600s (1 ч) | `CACHE_SEARCH_TTL` |
| Дивиденды | `dividendsTtl` | 86400s (24 ч) | `CACHE_DIVIDENDS_TTL` |

View File

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

View File

@ -1,63 +1,128 @@
# Database Schema
# Схема базы данных
## Overview
## Обзор
Используется **SQLite** через **Prisma ORM 7**. База данных находится в `apps/backend/dev.db`.
## Schema
## Схема
```prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
provider = "prisma-client-js"
}
datasource db {
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 {
id Int @id @default(autoincrement())
email String @unique
id Int @id @default(autoincrement())
email String @unique
password String
name String?
role String @default("user")
role String @default("user")
refreshToken String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
portfolios Portfolio[]
}
```
## Tables
## Таблицы
### Users
### User
| Column | Type | Constraints | Description |
| Колонка | Тип | Ограничения | Описание |
|--------|------|-------------|-------------|
| id | INTEGER | PK, AUTOINCREMENT | User ID |
| email | TEXT | UNIQUE, NOT NULL | Email address |
| password | TEXT | NOT NULL | bcrypt hash of password |
| name | TEXT | NULLABLE | Display name |
| role | TEXT | NOT NULL, DEFAULT 'user' | Role for RBAC |
| refreshToken | TEXT | NULLABLE | bcrypt hash of current refresh token |
| createdAt | DATETIME | NOT NULL | Timestamp |
| updatedAt | DATETIME | NOT NULL | Auto-updated timestamp |
| id | INTEGER | PK, AUTOINCREMENT | ID пользователя |
| email | TEXT | UNIQUE, NOT NULL | Email |
| password | TEXT | NOT NULL | bcrypt hash пароля |
| name | TEXT | NULLABLE | Отображаемое имя |
| role | TEXT | NOT NULL, DEFAULT 'user' | Роль для RBAC |
| refreshToken | TEXT | NULLABLE | bcrypt hash текущего refresh token |
| createdAt | DATETIME | NOT NULL | Дата создания |
| 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
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
npx prisma migrate dev --name <description>
```
To apply migrations in production:
Применить миграции в production:
```bash
npx prisma migrate deploy

View File

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

View File

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

View File

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

View File

@ -1,19 +1,19 @@
# Portfolio Module
# PortfolioModule
Portfolio module позволяет пользователям создавать и вести виртуальные инвестиционные портфели для
`PortfolioModule` позволяет пользователям создавать и вести виртуальные инвестиционные портфели для
аналитики и отслеживания позиций.
## Overview
## Обзор
- **Backend:** `PortfolioModule` (`apps/backend/src/modules/portfolio/`)
- **Frontend:** Protected pages at `/portfolios` and `/portfolios/:id`
- **Database:** `Portfolio` and `Position` models (Prisma + SQLite)
- **Frontend:** защищённые страницы `/portfolios` и `/portfolios/:id`
- **База данных:** модели `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` | POST | Создать портфель |
@ -25,7 +25,7 @@ Portfolio module позволяет пользователям создават
| `/api/v1/portfolios/:id/positions/:positionId` | PATCH | Обновить позицию |
| `/api/v1/portfolios/:id/positions/:positionId` | DELETE | Удалить позицию |
## Domain Model
## Доменная модель
```
Portfolio
@ -50,56 +50,63 @@ Position
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`.
- **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)
- `currentValue = (price / 100) * faceValue * quantity`
Bond enrichment also fetches:
| Field | Source | Description |
Обогащение облигаций также получает:
| Поле | Источник | Описание |
|---|---|---|
| `yieldToMaturity` | `getBondMarketData().yield` | YTM (%) |
| `duration` | `getBondMarketData().duration` | Modified duration (years) |
| `couponValue` | `getBondData().couponValue` | Coupon amount in RUB |
| `couponPercent` | `getBondData().couponPercent` | Coupon rate (%) |
| `couponPeriod` | `getBondData().couponPeriod` | Days between payments |
| `nextCouponDate` | `getBondData().nextCoupon` | Next coupon date |
| `matDate` | `getBondData().matDate` | Maturity date |
| `offerDate` | `getBondData().offerDate` | Early redemption date |
| `accruedInt` | `getBondData().accruedInt` | Accrued interest per bond (RUB) |
| `duration` | `getBondMarketData().duration` | Modified duration в годах |
| `couponValue` | `getBondData().couponValue` | Размер купона в RUB |
| `couponPercent` | `getBondData().couponPercent` | Ставка купона (%) |
| `couponPeriod` | `getBondData().couponPeriod` | Дней между выплатами |
| `nextCouponDate` | `getBondData().nextCoupon` | Дата следующего купона |
| `matDate` | `getBondData().matDate` | Дата погашения |
| `offerDate` | `getBondData().offerDate` | Дата досрочного погашения |
| `accruedInt` | `getBondData().accruedInt` | НКД на облигацию (RUB) |
| `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:
- **Shares:** `TQBR` (Т+: Акции — безадрес.)
- **Bonds:** `TQCB` (Т+: Корпоративные облигации — безадрес.)
Backend использует следующие MOEX boards по умолчанию:
- **Акции:** `TQBR` (Т+: Акции — безадрес.)
- **Облигации:** `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
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/`)
- **Frontend:** Search bar in layout, dedicated Screener page at `/screener`
- **Source:** MOEX ISS API
- **Frontend:** поисковая строка в layout и отдельная страница Screener по `/screener`
- **Источник:** MOEX ISS API
## Search API
`GET /api/v1/securities/search?q={query}&type={type}&limit={limit}`
Searches for securities by ticker or name.
- **type:** `all` (default), `share`, `bond`.
- **Results:** List of `SearchResultItem` with `secid`, `isin`, `shortName`, `type`, `listLevel`.
Ищет ценные бумаги по тикеру или названию.
- **type:** `all` по умолчанию, `share`, `bond`.
- **Результат:** список `SearchResultItem` с `secid`, `isin`, `shortName`, `type`, `listLevel`.
## Security 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. |
| `priceMin` / `Max` | number | Price range (RUB for shares, % for bonds) |
| `volumeMin` | number | Minimum daily trading volume |
| `yieldMin` / `Max` | number | YTM range (Bonds only) |
| `durationMin` / `Max`| number | Duration range in years (Bonds only) |
| `changePercentMin` | number | Minimum daily price change % (Shares only) |
| `sortBy` | string | Field to sort by (`price`, `volume`, `yieldToMaturity`, etc.) |
| `sortOrder` | `asc` \| `desc` | Sort direction |
| `page` / `pageSize` | number | Pagination parameters |
| `type` | `share` \| `bond` | **Обязателен.** Тип board для сканирования. |
| `priceMin` / `Max` | number | Диапазон цены: RUB для акций, проценты для облигаций |
| `volumeMin` | number | Минимальный дневной объём торгов |
| `yieldMin` / `Max` | number | Диапазон YTM, только для облигаций |
| `durationMin` / `Max`| number | Диапазон duration в годах, только для облигаций |
| `changePercentMin` | number | Минимальное дневное изменение цены в %, только для акций |
| `sortBy` | string | Поле сортировки: `price`, `volume`, `yieldToMaturity`, etc. |
| `sortOrder` | `asc` \| `desc` | Направление сортировки |
| `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-спецификации бэкенда.
### Generate Types
### Сгенерировать типы
```bash
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
```
### Prerequisites
### Требования
- Backend должен быть запущен на `localhost:3000`
- Swagger UI доступен на `http://localhost:3000/api/docs`
### Output
### Результат
- `apps/frontend/src/api/types.ts` — сгенерированные типы `paths` и `operations`
- Live Swagger JSON на `http://localhost:3000/api/docs-json` остаётся источником OpenAPI-контракта
### Verify 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
и не содержат локальный alternate port.
### Manual Types
### Ручные типы
Помимо 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:frontend` | Vite dev-сервер на `:5173`, проксирует `/api` на backend |
@ -16,9 +16,9 @@
| `npm run format` | 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 start:dev -w apps/backend` | `nest start --watch` |
@ -28,9 +28,9 @@
| `npm run test:watch -w apps/backend` | `vitest` |
| `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 build -w apps/frontend` | `tsc -b && vite build` |
@ -39,15 +39,15 @@
| `npm run lint -w apps/frontend` | ESLint для `src/**/*.{ts,tsx}` |
| `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 build -w apps/docs` | Production build документации |
| `npm run serve -w apps/docs` | Локальная проверка production build |
## Single Test
## Один тест
```bash
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`):
@ -26,17 +26,17 @@ ESLint запускается для backend (`apps/backend`) и frontend (`apps
Запуск: `npm run lint`
## Naming (backend)
## Именование в backend
- ПаскальКейс для модулей, контроллеров, сервисов
- `const` для всех переменных (кроме случаев, где нужна мутация)
- DTO-файлы в `dto/` внутри каждого модуля
## Imports (backend)
## Imports в backend
Алиас: `@/*``apps/backend/src/*`
## Imports (frontend)
## Imports в frontend
Алиас: `@/*``apps/frontend/src/*`
@ -50,7 +50,7 @@ ESLint запускается для backend (`apps/backend`) и frontend (`apps
- Strict: true
- ESModule interop: true
## Backend-specific
## Специфика backend
- Использует SWC через `unplugin-swc` (vitest config)
- Контроллеры содержат только маршрутизацию и вызовы сервисов

View File

@ -1,12 +1,12 @@
# Testing
# Тестирование
## Backend Tests
## Тесты backend
Фреймворк: **vitest** (с `unplugin-swc` для быстрой компиляции, не ts-jest).
Конфигурация vitest в `apps/backend/` использует SWC для трансформации TypeScript.
### Run All Tests
### Запустить все тесты
```bash
npm run test:backend
@ -14,21 +14,21 @@ npm run test:backend
npm run test -w apps/backend
```
### Run Single Test
### Запустить один тест
```bash
npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend
```
### Watch Mode
### Watch mode
```bash
npm run test:watch -w apps/backend
```
## Test Files
## Тестовые файлы
| File | What it tests |
| Файл | Что проверяет |
|---|---|
| `shares.service.spec.ts` | SharesService |
| `bonds.service.spec.ts` | BondsService |
@ -37,7 +37,7 @@ npm run test:watch -w apps/backend
| `securities.service.spec.ts` | SecuritiesService |
| `moex-client.service.spec.ts` | MoexClientService |
## Frontend Tests
## Тесты frontend
Фреймворк: **Vitest 4** + **React Testing Library** + **MSW**.
@ -51,7 +51,7 @@ npm run test -w apps/frontend
Тесты покрывают API-клиент, auth context, hooks, базовые pages и shared components.
## Live MOEX Integration Tests
## Live MOEX integration tests
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>`.
@ -16,7 +16,7 @@ async function request<T>(
- Парсит JSON-ответ в `ApiEnvelope<{ data: T, meta: ApiResponseMeta }>`
- Выбрасывает `Error` при HTTP-ошибке
## API Functions
## API functions
| Function | Method | Path |
|---|---|---|
@ -48,7 +48,7 @@ async function request<T>(
| `removePosition(portfolioId, positionId)` | DELETE | `/api/v1/portfolios/:id/positions/:positionId` |
| `getPortfolioAnalytics(portfolioId)` | GET | `/api/v1/portfolios/:id/analytics` |
## Types
## Типы
Ручные типы ответов в `api/responses.ts`:

View File

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

View File

@ -1,8 +1,8 @@
# Hooks
# Хуки
Все хуки используют TanStack Query v5.
| Hook | Query Key | Stale Time | Description |
| Хук | Query key | Stale time | Описание |
|---|---|---|---|
| `useSearch(query)` | `['securities', 'search', query]` | 60s | Поиск инструментов (enabled: query ≥ 2 символов) |
| `useStock(secid)` | `['stock', secid]` | 900s | Спецификация акции |
@ -11,7 +11,7 @@
| `useBond(secid)` | `['bond', secid]` | 900s | Спецификация облигации |
| `useBondCandles(secid, interval, from, till)` | `['bondCandles', secid, interval, from, till]` | 3600s | Свечи облигации |
## Query Configuration
## Конфигурация Query
```typescript
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.
## Tech Stack
## Технологический стек
- React 18
- react-router-dom v6
@ -12,11 +12,11 @@ React SPA, собранная с Vite.
- Vitest + Testing Library + MSW
- Vite 5
## Source Layout
## Структура исходников
```
apps/frontend/src/
├── main.tsx # Entry point
├── main.tsx # Точка входа
├── App.tsx # BrowserRouter
├── routes.tsx # Маршруты
├── api/
@ -65,6 +65,6 @@ apps/frontend/src/
└── styles.css
```
## Entry Point
## Точка входа
`main.tsx` рендерит `App.tsx`, который содержит `BrowserRouter``AppRoutes`.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,21 +6,21 @@ slug: /
Веб-приложение для анализа ценных бумаг Московской биржи (MOEX).
## Tech Stack
## Технологический стек
- **Backend:** NestJS, TypeScript, OpenAPI (Swagger)
- **Frontend:** React 18, TypeScript, Vite, TanStack Query v5, lightweight-charts v4
- **Infrastructure:** Docker, docker-compose
- **Инфраструктура:** Docker, docker-compose
- **CI:** Gitea Actions
## Repository Structure
## Структура репозитория
```
moex-vibe/
├── apps/
│ ├── backend/ # NestJS API (единственная точка доступа к MOEX)
│ ├── frontend/ # React SPA
│ └── docs/ # Docusaurus documentation site
│ └── docs/ # Docusaurus-сайт документации
├── docs/
│ └── superpowers/
│ └── specs/ # Согласованные SDD-спецификации
@ -32,7 +32,7 @@ moex-vibe/
└── tsconfig.base.json
```
## Key Principles
## Ключевые принципы
- Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую.
- 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 = {
title: 'MoexVibe',
tagline: 'Analysis of MOEX securities',
tagline: 'Анализ ценных бумаг MOEX',
favicon: 'img/favicon.ico',
url: 'https://moexvibe.example.com',

View File

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