docs: translate docs to russian
All checks were successful
All checks were successful
This commit is contained in:
parent
739405a597
commit
106467e5c4
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 возвращаются кешированные данные
|
||||
|
||||
@ -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
|
||||
## Последствия
|
||||
- Чёткие границы, изолированное тестирование
|
||||
- Возможность вынести модуль в отдельный микросервис
|
||||
- Понятная навигация по коду
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) в первой версии
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 очищает позиции при удалении портфеля
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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-разделе.
|
||||
|
||||
@ -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`)
|
||||
|
||||
@ -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 | Создать портфель |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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` |
|
||||
|
||||
@ -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`:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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`:
|
||||
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'овым и поддерживаются вручную.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
- Контроллеры содержат только маршрутизацию и вызовы сервисов
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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`:
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Components
|
||||
# Компоненты
|
||||
|
||||
## Layout (`components/Layout.tsx`)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Каждый хук:
|
||||
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
# Styling
|
||||
# Стили
|
||||
|
||||
## Approach
|
||||
## Подход
|
||||
|
||||
CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind.
|
||||
|
||||
## CSS Custom Properties
|
||||
## CSS custom properties
|
||||
|
||||
Определены в `styles.css`:
|
||||
|
||||
|
||||
@ -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'ами
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# CI/CD
|
||||
|
||||
## CI Pipeline
|
||||
## CI pipeline
|
||||
|
||||
Определён в `.gitea/workflows/ci.yml`. Запускается на push/PR в `main`.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user