docs: add Docusaurus documentation site as npm workspace
All checks were successful
All checks were successful
- Create apps/docs/ with Docusaurus 3.7.0 - Add documentation: architecture, backend, frontend, infrastructure, development, ADR - Include 5 Mermaid diagrams (system architecture, request flow, modules, caching, deployment) - Configure as npm workspace with dev:docs/build:docs scripts - Copy existing ADR documents from docs/architecture/adr/
This commit is contained in:
parent
a6d4b25afe
commit
aac5b2f873
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ dist/
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
vite.config.d.ts
|
vite.config.d.ts
|
||||||
vite.config.js
|
vite.config.js
|
||||||
|
apps/docs/.docusaurus/
|
||||||
|
apps/docs/build/
|
||||||
|
|||||||
3
apps/docs/babel.config.js
Normal file
3
apps/docs/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||||
|
};
|
||||||
25
apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md
Normal file
25
apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# ADR-001: Backend — Single Point of Access to MOEX
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect, Tech Lead
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Frontend должен отображать данные Московской биржи. MOEX ISS API отдаёт сырые данные со сложной структурой (вложенные таблицы, различные форматы). Прямые запросы с фронта приведут к дублированию логики нормализации, усложнят обработку ошибок и сделают систему зависимой от внешнего API.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Backend (NestJS) является единственной точкой доступа к MOEX. Frontend никогда не обращается к MOEX напрямую.
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
- Проксирует запросы к MOEX ISS
|
||||||
|
- Нормализует данные в доменные модели
|
||||||
|
- Кеширует ответы
|
||||||
|
- Обрабатывает ошибки MOEX (пустые данные, rate limit, таймауты)
|
||||||
|
- Предоставляет собственный OpenAPI-контракт для фронта
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Единый источник правды для трансформации данных
|
||||||
|
- Изоляция изменений MOEX API — меняется только MoexClient
|
||||||
|
- Централизованное кеширование сокращает количество запросов к MOEX
|
||||||
|
- Фронтенд остаётся тонким клиентом
|
||||||
|
- Дополнительная задержка (один hop), но нивелируется кешированием
|
||||||
33
apps/docs/docs/adr/ADR-002-in-memory-cache.md
Normal file
33
apps/docs/docs/adr/ADR-002-in-memory-cache.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# ADR-002: In-Memory Cache with Migration Path to Redis
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect, Tech Lead
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Для MVP требуется кеширование MOEX-данных, чтобы снизить нагрузку на внешнее API и обеспечить приемлемое время ответа. На начальном этапе нет требований к горизонтальному масштабированию, и хочется избежать внешних зависимостей.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Использовать `@nestjs/cache-manager` с MemoryStore. TTL настраивается per-endpoint через конфигурацию.
|
||||||
|
|
||||||
|
Архитектура позволяет переключиться на Redis заменой импорта провайдера:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Текущая реализация
|
||||||
|
CacheModule.register({ store: 'memory', ttl: 900 })
|
||||||
|
|
||||||
|
// Миграция на Redis (меняется только registration)
|
||||||
|
CacheModule.registerAsync({
|
||||||
|
useFactory: () => ({
|
||||||
|
store: redisStore,
|
||||||
|
host: process.env.REDIS_HOST,
|
||||||
|
port: process.env.REDIS_PORT,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Нет внешних зависимостей для MVP
|
||||||
|
- Кеш сбрасывается при рестарте сервера (приемлемо для read-only приложения)
|
||||||
|
- Чистый путь миграции на Redis
|
||||||
|
- Единый API для cache (cache-manager abstraction)
|
||||||
21
apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md
Normal file
21
apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# ADR-003: Rate Limiting Strategy for MOEX Client
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect, Tech Lead
|
||||||
|
|
||||||
|
## Context
|
||||||
|
MOEX ISS не документирует жёсткие лимиты на количество запросов, но массовые запросы могут привести к блокировке или ухудшению качества обслуживания. Backend является единственным клиентом MOEX и должен контролировать исходящий трафик.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Внедрить два механизма в MoexClient:
|
||||||
|
|
||||||
|
1. **Request Queue (p-queue)**: конфигурируемый лимит запросов в секунду (default: 10 req/s). Запросы сверх лимита ставятся в очередь и выполняются по расписанию.
|
||||||
|
|
||||||
|
2. **Circuit Breaker (`@nestjs/axios` + interceptor)**: при 5+ последовательных ошибках (5xx, timeout, network error) клиент перестаёт отправлять запросы к MOEX на 30 секунд. После таймаута — пробный запрос для восстановления.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Плавная нагрузка на MOEX, без пиков
|
||||||
|
- Автоматическое восстановление после сбоев MOEX
|
||||||
|
- Graceful degradation: при отключённом circuit breaker возвращаются кешированные данные
|
||||||
|
- Параметр конфигурации `MOEX_RATE_LIMIT` (int, req/s)
|
||||||
29
apps/docs/docs/adr/ADR-004-feature-modules.md
Normal file
29
apps/docs/docs/adr/ADR-004-feature-modules.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# ADR-004: Feature Modules by Domain
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect, Tech Lead
|
||||||
|
|
||||||
|
## Context
|
||||||
|
NestJS рекомендует модульную архитектуру. Требования указывают на архитектуру по feature modules. Модули должны иметь чёткие границы и быть тестируемыми изолированно.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Каждый бизнес-домен — отдельный NestJS feature module:
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
|--------|---------------|
|
||||||
|
| `MoexClientModule` | HTTP-клиент к MOEX ISS, rate limiting, circuit breaker |
|
||||||
|
| `CacheModule` | Абстракция кеширования |
|
||||||
|
| `SecuritiesModule` | Поиск по инструментам |
|
||||||
|
| `SharesModule` | Спецификация, marketdata, дивиденды |
|
||||||
|
| `BondsModule` | Спецификация, marketdata |
|
||||||
|
| `CandlesModule` | OHLCV свечи (общий для shares+bonds) |
|
||||||
|
| `HealthModule` | Healthcheck endpoint |
|
||||||
|
|
||||||
|
Каждый module exports свой сервис, control imports через `@Module({ imports: [...] })`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Чёткие границы, изолированное тестирование
|
||||||
|
- Возможность вынести модуль в отдельный микросервис
|
||||||
|
- Понятная навигация по коду
|
||||||
|
- Нет циклических зависимостей (MoexClient — единственный downstream)
|
||||||
40
apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md
Normal file
40
apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# ADR-005: OpenAPI Codegen with openapi-typescript
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect, Tech Lead
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Frontend должен потреблять API бэкенда. Ручное написание клиентов и DTO приводит к рассинхронизации с бэкендом и ошибкам типизации.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Использовать `openapi-typescript` + `openapi-fetch` для генерации:
|
||||||
|
|
||||||
|
- TypeScript типов (DTO, request/response schemas)
|
||||||
|
- Fetcher клиента (типобезопасные вызовы)
|
||||||
|
|
||||||
|
Процесс:
|
||||||
|
1. Backend генерирует OpenAPI spec через `@nestjs/swagger`
|
||||||
|
2. `openapi-typescript` на фронте генерирует типы
|
||||||
|
3. `openapi-fetch` создаёт типобезопасный HTTP-клиент
|
||||||
|
4. Разработчик пишет TanStack Query hooks вручную поверх сгенерированного клиента
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Пример: типобезопасный хук
|
||||||
|
import { getSharesSecid } from '@/api/client';
|
||||||
|
import type { components } from '@/api/types';
|
||||||
|
|
||||||
|
export function useStock(secid: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['stock', secid],
|
||||||
|
queryFn: () => getSharesSecid(secid),
|
||||||
|
staleTime: 900_000, // 15 min
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Полная типобезопасность на стыке frontend/backend
|
||||||
|
- Автоматическая синхронизация с API-контрактом
|
||||||
|
- TanStack Query hooks пишутся вручную — полный контроль staleTime/caching
|
||||||
|
- Добавляется шаг в CI: codegen при изменении OpenAPI spec
|
||||||
20
apps/docs/docs/adr/ADR-006-no-cci.md
Normal file
20
apps/docs/docs/adr/ADR-006-no-cci.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# ADR-006: CCI (Financial Reporting) Moved Out of MVP
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect, Product
|
||||||
|
|
||||||
|
## Context
|
||||||
|
MOEX предоставляет корпоративную информацию (CCI) — финансовую отчётность по МСФО/РСБУ. Данные включают отчёты о прибылях/убытках, балансовые отчёты, мультипликаторы. Однако:
|
||||||
|
|
||||||
|
- CCI API имеет собственную сложную структуру (виды отчётности, периоды, индикаторы)
|
||||||
|
- Данные требуют дополнительной нормализации и расчёта метрик
|
||||||
|
- Для MVP пользователи хотят базовую информацию (цена, купон, график)
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Не включать CCI в MVP. Roadmap на post-MVP.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Меньший объём работы в MVP
|
||||||
|
- API не привязывается к CCI-схемам (будет отдельный модуль)
|
||||||
|
- Пользователи не увидят мультипликаторы (P/E, EV/EBITDA) в первой версии
|
||||||
27
apps/docs/docs/adr/ADR-007-two-level-caching.md
Normal file
27
apps/docs/docs/adr/ADR-007-two-level-caching.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# ADR-007: Two-Level Caching (Backend + Frontend)
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Deciders:** Architect
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Данные MOEX имеют задержку 15 минут. Кеширование на одном уровне (только бэкенд или только фронтенд) неоптимально:
|
||||||
|
- Только бэкенд: каждый пользователь создаёт запрос к серверу
|
||||||
|
- Только фронтенд: нет централизованного кеша, не защищает MOEX от повторных запросов
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Внедрить два уровня кеширования:
|
||||||
|
|
||||||
|
1. **Backend (in-memory cache-manager)**: централизованное кеширование ответов от MOEX. Предотвращает повторные запросы к MOEX от разных пользователей.
|
||||||
|
|
||||||
|
2. **Frontend (TanStack Query staleTime)**: предотвращает повторные запросы к бэкенду при навигации или монтировании компонентов.
|
||||||
|
|
||||||
|
TTL согласованы (см. Caching Strategy).
|
||||||
|
|
||||||
|
Cache-Control заголовки в HTTP-ответах для промежуточных proxy/CDN (опционально).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Избыточность intentional: resilience при отказе одного уровня
|
||||||
|
- TanStack Query staleTime = backend TTL (нет лишних запросов)
|
||||||
|
- При рестарте бэкенда фронт всё ещё имеет данные в memory cache
|
||||||
|
- Небольшое увеличение memory на фронте (приемлемо для SPA)
|
||||||
13
apps/docs/docs/adr/index.md
Normal file
13
apps/docs/docs/adr/index.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
| ADR | Status | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| [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 находятся в `docs/architecture/adr/`.
|
||||||
110
apps/docs/docs/architecture.md
Normal file
110
apps/docs/docs/architecture.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Browser["Browser<br/>(React SPA)"]
|
||||||
|
Backend["NestJS API<br/>:3000"]
|
||||||
|
Cache["In-Memory Cache<br/>(cache-manager)"]
|
||||||
|
MOEX["MOEX ISS API<br/>iss.moex.com"]
|
||||||
|
|
||||||
|
Browser -->|"/api/v1/*"| Backend
|
||||||
|
Backend -->|"getOrFetch()"| Cache
|
||||||
|
Backend -->|"GET /iss/*.json"| MOEX
|
||||||
|
Cache -->|"data"| Backend
|
||||||
|
MOEX -->|"raw data"| Backend
|
||||||
|
|
||||||
|
subgraph Backend_Internal["Backend (NestJS)"]
|
||||||
|
MoexClient["MoexClientModule<br/>p-queue + circuit breaker"]
|
||||||
|
Shares["SharesModule"]
|
||||||
|
Bonds["BondsModule"]
|
||||||
|
Candles["CandlesModule"]
|
||||||
|
Securities["SecuritiesModule"]
|
||||||
|
Health["HealthModule"]
|
||||||
|
CacheService["CacheService<br/>(global)"]
|
||||||
|
|
||||||
|
MoexClient -->|"fetches"| Shares
|
||||||
|
MoexClient -->|"fetches"| Bonds
|
||||||
|
MoexClient -->|"fetches"| Candles
|
||||||
|
MoexClient -->|"fetches"| Securities
|
||||||
|
Shares -->|"uses"| CacheService
|
||||||
|
Bonds -->|"uses"| CacheService
|
||||||
|
Candles -->|"uses"| CacheService
|
||||||
|
Securities -->|"uses"| CacheService
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Frontend as React SPA
|
||||||
|
participant Backend as NestJS API
|
||||||
|
participant Cache as In-Memory Cache
|
||||||
|
participant MOEX as MOEX ISS
|
||||||
|
|
||||||
|
User->>Frontend: Search / View instrument
|
||||||
|
Frontend->>Backend: GET /api/v1/securities/search?q=SBER
|
||||||
|
Backend->>Cache: getOrFetch('search:sber')
|
||||||
|
alt Cache miss
|
||||||
|
Cache->>Backend: null
|
||||||
|
Backend->>MOEX: GET /iss/securities?q=SBER
|
||||||
|
MOEX-->>Backend: raw data
|
||||||
|
Backend->>Cache: set('search:sber', normalized, TTL=3600)
|
||||||
|
else Cache hit
|
||||||
|
Cache-->>Backend: cached data
|
||||||
|
end
|
||||||
|
Backend-->>Frontend: { data, meta: { fromCache, cachedAt } }
|
||||||
|
Frontend-->>User: rendered UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
All architectural decisions are documented as ADR in `docs/architecture/adr/`:
|
||||||
|
|
||||||
|
| ADR | Summary |
|
||||||
|
|---|---|
|
||||||
|
| [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 |
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
Все ответы API обёрнуты в единый формат:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": { ... },
|
||||||
|
"meta": {
|
||||||
|
"fromCache": false,
|
||||||
|
"cachedAt": "2026-06-13T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
При ошибках возвращается:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 404,
|
||||||
|
"message": "Share SBER not found",
|
||||||
|
"error": "Not Found",
|
||||||
|
"timestamp": "2026-06-13T12:00:00.000Z",
|
||||||
|
"path": "/api/v1/securities/shares/SBER"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Configuration
|
||||||
|
|
||||||
|
- Global prefix: `/api/v1`
|
||||||
|
- ValidationPipe: `transform: true, whitelist: true`
|
||||||
|
- HttpExceptionFilter (catch-all)
|
||||||
|
- TransformInterceptor (авто-обёртка в `ApiResponse`)
|
||||||
|
- RequestLoggingMiddleware (логирует `METHOD /path STATUS DURATIONms`)
|
||||||
|
- CORS: разрешён для всех origins
|
||||||
275
apps/docs/docs/backend/api.md
Normal file
275
apps/docs/docs/backend/api.md
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
Все эндпоинты находятся под префиксом `/api/v1`. Swagger UI: `/api/docs`.
|
||||||
|
|
||||||
|
## Health
|
||||||
|
|
||||||
|
### `GET /health`
|
||||||
|
|
||||||
|
Проверка состояния сервиса.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": "2026-06-13T12:00:00.000Z",
|
||||||
|
"uptime": 1234.56
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Securities
|
||||||
|
|
||||||
|
### `GET /securities/search`
|
||||||
|
|
||||||
|
Поиск по инструментам.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Param | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `q` | string | yes | — | Поисковый запрос (1-100 символов) |
|
||||||
|
| `type` | enum | no | `all` | Фильтр: `all`, `share`, `bond` |
|
||||||
|
| `limit` | integer | no | `20` | Лимит результатов |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"secid": "SBER",
|
||||||
|
"isin": "RU0009029540",
|
||||||
|
"shortName": "Сбербанк",
|
||||||
|
"type": "share",
|
||||||
|
"listLevel": 1,
|
||||||
|
"currency": "RUB",
|
||||||
|
"price": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shares
|
||||||
|
|
||||||
|
### `GET /securities/shares/:secid`
|
||||||
|
|
||||||
|
Спецификация акции.
|
||||||
|
|
||||||
|
**Parameters:** `secid` — тикер (например, `SBER`)
|
||||||
|
|
||||||
|
**Response:** `ShareResponse` — спецификация + текущие рыночные данные.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"secid": "SBER",
|
||||||
|
"isin": "RU0009029540",
|
||||||
|
"name": "Сбербанк России ПАО ао",
|
||||||
|
"shortName": "Сбербанк",
|
||||||
|
"latName": null,
|
||||||
|
"listLevel": 1,
|
||||||
|
"issueSize": 21586948000,
|
||||||
|
"faceValue": 3,
|
||||||
|
"faceUnit": "RUB",
|
||||||
|
"type": "common_share",
|
||||||
|
"marketData": {
|
||||||
|
"price": 322.35,
|
||||||
|
"change": 1.15,
|
||||||
|
"changePercent": 0.36,
|
||||||
|
"open": 321.3,
|
||||||
|
"high": 322.66,
|
||||||
|
"low": 321.2,
|
||||||
|
"volume": 1925163,
|
||||||
|
"value": 620184479,
|
||||||
|
"issueCapitalization": 6958336818320,
|
||||||
|
"updatedAt": "2026-06-13T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /securities/shares/:secid/marketdata`
|
||||||
|
|
||||||
|
Рыночные данные акции (без спецификации).
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"price": 322.35,
|
||||||
|
"change": 1.15,
|
||||||
|
"changePercent": 0.36,
|
||||||
|
"open": 321.3,
|
||||||
|
"high": 322.66,
|
||||||
|
"low": 321.2,
|
||||||
|
"volume": 1925163,
|
||||||
|
"value": 620184479,
|
||||||
|
"issueCapitalization": 6958336818320,
|
||||||
|
"updatedAt": "2026-06-13T10:30:00.000Z"
|
||||||
|
},
|
||||||
|
"meta": { "fromCache": true, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /securities/shares/:secid/dividends`
|
||||||
|
|
||||||
|
Дивиденды акции.
|
||||||
|
|
||||||
|
**Parameters:** `secid` — тикер
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"registryCloseDate": "2026-07-10",
|
||||||
|
"value": 33.4,
|
||||||
|
"currency": "RUB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /securities/shares/:secid/history`
|
||||||
|
|
||||||
|
Дневная история торгов акции.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Param | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `from` | string (date) | yes | Начальная дата (`YYYY-MM-DD`) |
|
||||||
|
| `till` | string (date) | yes | Конечная дата (`YYYY-MM-DD`) |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"date": "2026-06-13",
|
||||||
|
"open": 321.3,
|
||||||
|
"high": 322.66,
|
||||||
|
"low": 321.2,
|
||||||
|
"close": 322.35,
|
||||||
|
"volume": 1925163,
|
||||||
|
"value": 620184479
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bonds
|
||||||
|
|
||||||
|
### `GET /securities/bonds/:secid`
|
||||||
|
|
||||||
|
Спецификация облигации.
|
||||||
|
|
||||||
|
**Parameters:** `secid` — тикер
|
||||||
|
|
||||||
|
**Response:** `BondResponse` — спецификация + рыночные данные.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"secid": "SU26238RMFS5",
|
||||||
|
"isin": "RU000A106ZJ4",
|
||||||
|
"name": "ОФЗ 26238",
|
||||||
|
"shortName": "ОФЗ 26238",
|
||||||
|
"latName": null,
|
||||||
|
"listLevel": 1,
|
||||||
|
"issueSize": 500000000,
|
||||||
|
"faceValue": 1000,
|
||||||
|
"faceUnit": "RUB",
|
||||||
|
"matDate": "2041-05-15",
|
||||||
|
"couponValue": 34.9,
|
||||||
|
"couponPercent": 6.98,
|
||||||
|
"couponPeriod": 182,
|
||||||
|
"nextCoupon": "2026-12-01",
|
||||||
|
"accruedInt": 12.45,
|
||||||
|
"bondType": "OFZ",
|
||||||
|
"bondSubType": "",
|
||||||
|
"offerDate": null,
|
||||||
|
"buybackDate": null,
|
||||||
|
"marketData": {
|
||||||
|
"price": 98.45,
|
||||||
|
"yieldToMaturity": 7.12,
|
||||||
|
"duration": 8.34,
|
||||||
|
"accruedInt": 12.45,
|
||||||
|
"couponValue": 34.9,
|
||||||
|
"couponPercent": 6.98,
|
||||||
|
"nextCouponDate": "2026-12-01",
|
||||||
|
"open": 98.3,
|
||||||
|
"high": 98.6,
|
||||||
|
"low": 98.2,
|
||||||
|
"volume": 1500000,
|
||||||
|
"updatedAt": "2026-06-13T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /securities/bonds/:secid/marketdata`
|
||||||
|
|
||||||
|
Рыночные данные облигации.
|
||||||
|
|
||||||
|
### `GET /securities/bonds/:secid/history`
|
||||||
|
|
||||||
|
Дневная история торгов облигации.
|
||||||
|
|
||||||
|
**Parameters:** `from`, `till` (date)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"date": "2026-06-13",
|
||||||
|
"closePrice": 98.45,
|
||||||
|
"yieldClose": 7.12,
|
||||||
|
"duration": 8.34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Candles
|
||||||
|
|
||||||
|
### `GET /securities/shares/:secid/candles`
|
||||||
|
|
||||||
|
Свечи акции.
|
||||||
|
|
||||||
|
### `GET /securities/bonds/:secid/candles`
|
||||||
|
|
||||||
|
Свечи облигации.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Param | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `interval` | enum | yes | `1h` или `24h` |
|
||||||
|
| `from` | string (date) | yes | Начальная дата |
|
||||||
|
| `till` | string (date) | yes | Конечная дата |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"open": 321.3,
|
||||||
|
"high": 322.66,
|
||||||
|
"low": 321.2,
|
||||||
|
"close": 322.35,
|
||||||
|
"volume": 1925163,
|
||||||
|
"value": 620184479,
|
||||||
|
"begin": "2026-06-13T10:00:00",
|
||||||
|
"end": "2026-06-13T10:59:59"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": { "fromCache": false, "cachedAt": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
80
apps/docs/docs/backend/caching.md
Normal file
80
apps/docs/docs/backend/caching.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Caching
|
||||||
|
|
||||||
|
## Cache Strategy
|
||||||
|
|
||||||
|
In-memory кеш через `@nestjs/cache-manager` (cache-manager v5). Поддерживается миграция на Redis (см. ADR-002).
|
||||||
|
|
||||||
|
## Cache Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Request["Request Data"]
|
||||||
|
CacheService["CacheService.getOrFetch()"]
|
||||||
|
BuildKey["buildKey(prefix, keyParts)"]
|
||||||
|
CacheGet["cacheManager.get(key)"]
|
||||||
|
Hit["Cache HIT → return data"]
|
||||||
|
Miss["Cache MISS"]
|
||||||
|
FetchFn["FetchFn() → get from MOEX"]
|
||||||
|
CacheSet["cacheManager.set(key, data, ttl)"]
|
||||||
|
Return["Return { data, fromCache, cachedAt }"]
|
||||||
|
|
||||||
|
Request --> CacheService
|
||||||
|
CacheService --> BuildKey
|
||||||
|
BuildKey --> CacheGet
|
||||||
|
CacheGet --> Hit
|
||||||
|
CacheGet --> Miss
|
||||||
|
Miss --> FetchFn
|
||||||
|
FetchFn --> CacheSet
|
||||||
|
CacheSet --> Return
|
||||||
|
Hit --> Return
|
||||||
|
```
|
||||||
|
|
||||||
|
## CacheService
|
||||||
|
|
||||||
|
`apps/backend/src/modules/cache/cache.service.ts`
|
||||||
|
|
||||||
|
Основной метод:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
getOrFetch<T>(
|
||||||
|
keyPrefix: string, // 'marketdata' | 'history' | 'candles' | 'search' | 'bond' | 'dividends'
|
||||||
|
keyParts: string[], // ['shares', 'SBER'] | ['SBER', '2026-06-13', '2026-06-14']
|
||||||
|
fetchFn: () => Promise<T>,
|
||||||
|
ttlConfigKey: string, // 'marketDataTtl' | 'historyTtl' | etc.
|
||||||
|
): Promise<{ data: T; fromCache: boolean; cachedAt: string | null }>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ключ строится как `keyPrefix:keyPart1:keyPart2:...`
|
||||||
|
- При cache hit возвращает `{ data, fromCache: true, cachedAt: null }`
|
||||||
|
- При cache miss вызывает `fetchFn`, сохраняет результат с TTL и возвращает `{ data, fromCache: false, cachedAt: '...' }`
|
||||||
|
|
||||||
|
## Cache Module Configuration
|
||||||
|
|
||||||
|
`apps/backend/src/modules/cache/cache.module.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
NestCacheModule.register({
|
||||||
|
ttl: 900,
|
||||||
|
max: 1000,
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [CacheService],
|
||||||
|
exports: [CacheService],
|
||||||
|
})
|
||||||
|
export class CacheModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per-Data TTL
|
||||||
|
|
||||||
|
| Data Type | Config Key | Default TTL | Config Variable |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 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` |
|
||||||
45
apps/docs/docs/backend/configuration.md
Normal file
45
apps/docs/docs/backend/configuration.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 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 |
|
||||||
|
| `MOEX_RATE_LIMIT` | `10` | Максимум запросов в секунду к MOEX |
|
||||||
|
| `MOEX_CIRCUIT_BREAKER_THRESHOLD` | `5` | Количество ошибок до открытия circuit breaker |
|
||||||
|
| `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` | `30` | Время в секундах до сброса circuit breaker |
|
||||||
|
| `CACHE_MARKET_DATA_TTL` | `900` | TTL рыночных данных (секунды) |
|
||||||
|
| `CACHE_HISTORY_TTL` | `3600` | TTL истории торгов (секунды) |
|
||||||
|
| `CACHE_CANDLES_TTL` | `3600` | TTL свечей (секунды) |
|
||||||
|
| `CACHE_SECURITY_TTL` | `86400` | TTL спецификации инструмента (секунды) |
|
||||||
|
| `CACHE_SEARCH_TTL` | `3600` | TTL результатов поиска (секунды) |
|
||||||
|
| `CACHE_DIVIDENDS_TTL` | `86400` | TTL дивидендов (секунды) |
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
`apps/backend/src/config/configuration.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerAs('app', () => ({
|
||||||
|
port: parseInt(process.env.PORT || '3000', 10),
|
||||||
|
moex: {
|
||||||
|
baseUrl: process.env.MOEX_BASE_URL || 'https://iss.moex.com/iss',
|
||||||
|
rateLimit: parseInt(process.env.MOEX_RATE_LIMIT || '10', 10),
|
||||||
|
circuitBreakerThreshold: parseInt(process.env.MOEX_CIRCUIT_BREAKER_THRESHOLD || '5', 10),
|
||||||
|
circuitBreakerResetSeconds: parseInt(
|
||||||
|
process.env.MOEX_CIRCUIT_BREAKER_RESET_SECONDS || '30', 10,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
marketDataTtl: parseInt(process.env.CACHE_MARKET_DATA_TTL || '900', 10),
|
||||||
|
historyTtl: parseInt(process.env.CACHE_HISTORY_TTL || '3600', 10),
|
||||||
|
candlesTtl: parseInt(process.env.CACHE_CANDLES_TTL || '3600', 10),
|
||||||
|
securityTtl: parseInt(process.env.CACHE_SECURITY_TTL || '86400', 10),
|
||||||
|
searchTtl: parseInt(process.env.CACHE_SEARCH_TTL || '3600', 10),
|
||||||
|
dividendsTtl: parseInt(process.env.CACHE_DIVIDENDS_TTL || '86400', 10),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
94
apps/docs/docs/backend/modules.md
Normal file
94
apps/docs/docs/backend/modules.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Backend Modules
|
||||||
|
|
||||||
|
## Module Dependency Graph
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
AppModule --> ConfigModule
|
||||||
|
AppModule --> CacheModule
|
||||||
|
AppModule --> MoexClientModule
|
||||||
|
AppModule --> HealthModule
|
||||||
|
AppModule --> SecuritiesModule
|
||||||
|
AppModule --> SharesModule
|
||||||
|
AppModule --> BondsModule
|
||||||
|
AppModule --> CandlesModule
|
||||||
|
|
||||||
|
subgraph Global_Modules["Global Modules"]
|
||||||
|
CacheModule
|
||||||
|
MoexClientModule
|
||||||
|
end
|
||||||
|
|
||||||
|
SharesModule --> CacheService["CacheService"]
|
||||||
|
BondsModule --> CacheService
|
||||||
|
SecuritiesModule --> CacheService
|
||||||
|
CandlesModule --> CacheService
|
||||||
|
|
||||||
|
SharesModule --> MoexClientService["MoexClientService"]
|
||||||
|
BondsModule --> MoexClientService
|
||||||
|
SecuritiesModule --> MoexClientService
|
||||||
|
CandlesModule --> MoexClientService
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module List
|
||||||
|
|
||||||
|
| Module | Global | Path | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `CacheModule` | Yes | `modules/cache/` | In-memory cache (cache-manager) |
|
||||||
|
| `MoexClientModule` | Yes | `modules/moex-client/` | HTTP-клиент MOEX ISS |
|
||||||
|
| `HealthModule` | No | `modules/health/` | Health check endpoint |
|
||||||
|
| `SecuritiesModule` | No | `modules/securities/` | Поиск инструментов |
|
||||||
|
| `SharesModule` | No | `modules/shares/` | Акции |
|
||||||
|
| `BondsModule` | No | `modules/bonds/` | Облигации |
|
||||||
|
| `CandlesModule` | No | `modules/candles/` | Свечи OHLCV |
|
||||||
|
|
||||||
|
### CacheModule
|
||||||
|
|
||||||
|
Глобальный in-memory кеш.
|
||||||
|
|
||||||
|
- Использует `@nestjs/cache-manager` со стандартным TTL 900s, максимум 1000 записей
|
||||||
|
- Предоставляет `CacheService` с методом `getOrFetch(prefix, keyParts, fetchFn, ttlConfigKey)`
|
||||||
|
- TTL настраивается через env-переменные `CACHE_*_TTL`
|
||||||
|
|
||||||
|
### MoexClientModule
|
||||||
|
|
||||||
|
Глобальный HTTP-клиент для MOEX ISS.
|
||||||
|
|
||||||
|
- Rate limiter: p-queue (10 req/s по умолчанию, настраивается через `MOEX_RATE_LIMIT`)
|
||||||
|
- Circuit breaker: открывается после 5 ошибок, сбрасывается через 30s
|
||||||
|
- Все ответы нормализуются из табличного формата MOEX в доменные типы
|
||||||
|
|
||||||
|
### HealthModule
|
||||||
|
|
||||||
|
Проверка состояния сервиса.
|
||||||
|
|
||||||
|
- `GET /api/v1/health` → `{ status: 'ok', timestamp, uptime }`
|
||||||
|
|
||||||
|
### SecuritiesModule
|
||||||
|
|
||||||
|
Поиск по инструментам (акции и облигации).
|
||||||
|
|
||||||
|
- Единственный эндпоинт: `GET /securities/search`
|
||||||
|
- Фильтрация по типу: `all`, `share`, `bond`
|
||||||
|
- Проверяет `group` и `type` поля из MOEX для классификации
|
||||||
|
|
||||||
|
### SharesModule
|
||||||
|
|
||||||
|
Работа с акциями.
|
||||||
|
|
||||||
|
- 4 эндпоинта: спецификация, marketdata, дивиденды, история
|
||||||
|
- Поддерживает board `TQBR` (основной режим торгов акций)
|
||||||
|
|
||||||
|
### BondsModule
|
||||||
|
|
||||||
|
Работа с облигациями.
|
||||||
|
|
||||||
|
- 3 эндпоинта: спецификация, marketdata, история
|
||||||
|
- Поддерживает board `TQCB` (основной режим торгов облигаций)
|
||||||
|
|
||||||
|
### CandlesModule
|
||||||
|
|
||||||
|
Получение свечных данных (OHLCV).
|
||||||
|
|
||||||
|
- 2 эндпоинта: для акций и облигаций
|
||||||
|
- Интервалы: `1h` (60 min) и `24h` (daily)
|
||||||
|
- Маппинг: `1h` → MOEX interval 60, `24h` → MOEX interval 24
|
||||||
84
apps/docs/docs/backend/moex-client.md
Normal file
84
apps/docs/docs/backend/moex-client.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# MOEX Client
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`MoexClientService` (`apps/backend/src/modules/moex-client/moex-client.service.ts`) — HTTP-клиент для MOEX ISS API.
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
Использует `p-queue`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
this.queue = new PQueue({
|
||||||
|
interval: 1000,
|
||||||
|
intervalCap: rateLimit, // 10 по умолчанию
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Все запросы к MOEX проходят через очередь — не более `MOEX_RATE_LIMIT` запросов в секунду.
|
||||||
|
|
||||||
|
## Circuit Breaker
|
||||||
|
|
||||||
|
Состояние: закрыт → открыт → полуоткрыт (через таймаут).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private circuitOpen = false;
|
||||||
|
private circuitErrorCount = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
- После `MOEX_CIRCUIT_BREAKER_THRESHOLD` (5) последовательных ошибок — открывается
|
||||||
|
- В открытом состоянии все запросы мгновенно падают с ошибкой `"Circuit breaker is open"`
|
||||||
|
- Через `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` (30) автоматически сбрасывается
|
||||||
|
|
||||||
|
## Request Method
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private async request<T>(path: string, params?: Record<string, string>): Promise<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Добавляет `.json` к пути (MOEX JSON API)
|
||||||
|
- Устанавливает `iss.meta=off` (отключает метаданные)
|
||||||
|
- Таймаут: 10s
|
||||||
|
|
||||||
|
## Response Parsing
|
||||||
|
|
||||||
|
MOEX возвращает данные в табличном формате:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"securities": {
|
||||||
|
"columns": ["secid", "isin", "name", ...],
|
||||||
|
"data": [["SBER", "RU0009029540", "Сбербанк", ...]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Метод `extractTable` преобразует это в массив объектов по колонкам.
|
||||||
|
|
||||||
|
## Available MOEX Methods
|
||||||
|
|
||||||
|
| Method | MOEX Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `searchSecurities` | `/securities?q=` | Поиск инструментов |
|
||||||
|
| `getSecurityDescription` | `/securities/{secid}` | Спецификация |
|
||||||
|
| `getShareMarketData` | `/engines/stock/markets/shares/securities/{secid}` | Рыночные данные акции (board: TQBR) |
|
||||||
|
| `getBondData` | `/engines/stock/markets/bonds/securities/{secid}` | Данные облигации (board: TQCB) |
|
||||||
|
| `getBondMarketData` | `/engines/stock/markets/bonds/securities/{secid}` | Рыночные данные облигации |
|
||||||
|
| `getDividends` | `/securities/{secid}/dividends` | Дивиденды |
|
||||||
|
| `getCandles` | `/engines/{engine}/markets/{market}/securities/{secid}/candles` | Свечи |
|
||||||
|
| `getHistory` | `/engines/stock/markets/shares/securities/{secid}` | История акций |
|
||||||
|
| `getBondHistory` | `/engines/stock/markets/bonds/securities/{secid}` | История облигаций |
|
||||||
|
|
||||||
|
## MOEX ISS Types
|
||||||
|
|
||||||
|
Все MOEX-типы описаны в `apps/backend/src/modules/moex-client/moex-client.types.ts`:
|
||||||
|
|
||||||
|
- `MoexSecurityDescription` — описание инструмента
|
||||||
|
- `MoexShareMarketData` — рыночные данные акции
|
||||||
|
- `MoexBondData` — данные облигации
|
||||||
|
- `MoexBondMarketData` — рыночные данные облигации
|
||||||
|
- `MoexDividend` — дивиденд
|
||||||
|
- `MoexCandle` — свеча (OHLCV)
|
||||||
|
- `MoexHistoryEntry` — запись истории акции
|
||||||
|
- `MoexBondHistoryEntry` — запись истории облигации
|
||||||
|
- `MoexBoard` — торговая доска
|
||||||
48
apps/docs/docs/backend/overview.md
Normal file
48
apps/docs/docs/backend/overview.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Backend Overview
|
||||||
|
|
||||||
|
Backend — NestJS-приложение, единственная точка доступа к MOEX ISS.
|
||||||
|
|
||||||
|
## Entry Point
|
||||||
|
|
||||||
|
`apps/backend/src/main.ts` — bootstrap:
|
||||||
|
|
||||||
|
- Глобальный префикс `/api/v1`
|
||||||
|
- Swagger на `/api/docs` (title: "MoexVibe API", version: "1.0.0")
|
||||||
|
- ValidationPipe, HttpExceptionFilter, TransformInterceptor, RequestLoggingMiddleware
|
||||||
|
- CORS включён
|
||||||
|
- Порт из `PORT` env (default: 3000)
|
||||||
|
|
||||||
|
## Root Module
|
||||||
|
|
||||||
|
`apps/backend/src/app.module.ts` импортирует:
|
||||||
|
|
||||||
|
- `ConfigModule.forRoot` (глобальный, из `config/configuration.ts`)
|
||||||
|
- Импортируются 6 feature-модулей (Cache + MoexClient — глобальные)
|
||||||
|
|
||||||
|
## Source Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/backend/src/
|
||||||
|
├── main.ts
|
||||||
|
├── app.module.ts
|
||||||
|
├── config/
|
||||||
|
│ └── configuration.ts
|
||||||
|
├── common/
|
||||||
|
│ ├── dto/
|
||||||
|
│ │ ├── api-response.dto.ts
|
||||||
|
│ │ └── pagination.dto.ts
|
||||||
|
│ ├── filters/
|
||||||
|
│ │ └── http-exception.filter.ts
|
||||||
|
│ ├── interceptors/
|
||||||
|
│ │ └── transform.interceptor.ts
|
||||||
|
│ └── middleware/
|
||||||
|
│ └── request-logging.middleware.ts
|
||||||
|
└── modules/
|
||||||
|
├── moex-client/
|
||||||
|
├── cache/
|
||||||
|
├── health/
|
||||||
|
├── securities/
|
||||||
|
├── shares/
|
||||||
|
├── bonds/
|
||||||
|
└── candles/
|
||||||
|
```
|
||||||
30
apps/docs/docs/development/codegen.md
Normal file
30
apps/docs/docs/development/codegen.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Code Generation
|
||||||
|
|
||||||
|
## OpenAPI Types
|
||||||
|
|
||||||
|
Frontend генерирует TypeScript-типы из Swagger-спецификации бэкенда.
|
||||||
|
|
||||||
|
### Generate Types
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run codegen -w apps/frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Выполняет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`
|
||||||
|
|
||||||
|
### Manual Types
|
||||||
|
|
||||||
|
Помимо codegen, используются рукописные типы в `apps/frontend/src/api/responses.ts`. Они не полностью соответствуют codegen'овым и поддерживаются вручную.
|
||||||
40
apps/docs/docs/development/commands.md
Normal file
40
apps/docs/docs/development/commands.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Commands
|
||||||
|
|
||||||
|
## Root Workspace
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 |
|
||||||
|
| `npm run dev:frontend` | Vite dev-сервер на :5173, проксирует `/api` → :3000 |
|
||||||
|
| `npm run build:backend` | `nest build` |
|
||||||
|
| `npm run build:frontend` | `tsc -b && vite build` (в две фазы) |
|
||||||
|
| `npm run test:backend` | `vitest run` (SWC, не ts-jest) |
|
||||||
|
| `npm run lint` | ESLint только для бэкенда |
|
||||||
|
| `npm run format` | Prettier для всех `*.{ts,tsx}` |
|
||||||
|
| `npm run format:check` | Prettier check для всех `*.{ts,tsx}` |
|
||||||
|
|
||||||
|
## Backend Workspace
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `npm run build -w apps/backend` | `nest build` |
|
||||||
|
| `npm run start:dev -w apps/backend` | `nest start --watch` |
|
||||||
|
| `npm run start:prod -w apps/backend` | `node dist/main` |
|
||||||
|
| `npm run lint -w apps/backend` | ESLint для `{src,test}/**/*.ts` |
|
||||||
|
| `npm run test -w apps/backend` | `vitest run` |
|
||||||
|
| `npm run test:watch -w apps/backend` | `vitest` |
|
||||||
|
|
||||||
|
## Frontend Workspace
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `npm run dev -w apps/frontend` | `vite` |
|
||||||
|
| `npm run build -w apps/frontend` | `tsc -b && vite build` |
|
||||||
|
| `npm run preview -w apps/frontend` | `vite preview` |
|
||||||
|
| `npm run codegen -w apps/frontend` | `openapi-typescript` из Swagger → `src/api/types.ts` |
|
||||||
|
|
||||||
|
## Single Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend
|
||||||
|
```
|
||||||
57
apps/docs/docs/development/conventions.md
Normal file
57
apps/docs/docs/development/conventions.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Code Conventions
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
Prettier (`.prettierrc`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка: `npm run format:check`
|
||||||
|
Применение: `npm run format`
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
ESLint только для бэкенда (`apps/backend`).
|
||||||
|
|
||||||
|
Плагины:
|
||||||
|
- `@typescript-eslint/eslint-plugin`
|
||||||
|
- `@typescript-eslint/parser`
|
||||||
|
|
||||||
|
Запуск: `npm run lint`
|
||||||
|
|
||||||
|
## Naming (backend)
|
||||||
|
|
||||||
|
- ПаскальКейс для модулей, контроллеров, сервисов
|
||||||
|
- `const` для всех переменных (кроме случаев, где нужна мутация)
|
||||||
|
- DTO-файлы в `dto/` внутри каждого модуля
|
||||||
|
|
||||||
|
## Imports (backend)
|
||||||
|
|
||||||
|
Алиас: `@/*` → `apps/backend/src/*`
|
||||||
|
|
||||||
|
## Imports (frontend)
|
||||||
|
|
||||||
|
Алиас: `@/*` → `apps/frontend/src/*`
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
Настройки из `tsconfig.base.json`:
|
||||||
|
|
||||||
|
- Target: ES2022
|
||||||
|
- Module: ESNext
|
||||||
|
- Module resolution: bundler
|
||||||
|
- Strict: true
|
||||||
|
- ESModule interop: true
|
||||||
|
|
||||||
|
## Backend-specific
|
||||||
|
|
||||||
|
- Использует SWC через `unplugin-swc` (vitest config)
|
||||||
|
- Контроллеры содержат только маршрутизацию и вызовы сервисов
|
||||||
|
- Сервисы содержат бизнес-логику, вызовы MoexClient и кеширование
|
||||||
42
apps/docs/docs/development/testing.md
Normal file
42
apps/docs/docs/development/testing.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Testing
|
||||||
|
|
||||||
|
## Backend Tests
|
||||||
|
|
||||||
|
Фреймворк: **vitest** (с `unplugin-swc` для быстрой компиляции, не ts-jest).
|
||||||
|
|
||||||
|
Конфигурация vitest в `apps/backend/` использует SWC для трансформации TypeScript.
|
||||||
|
|
||||||
|
### Run All Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:watch -w apps/backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Files
|
||||||
|
|
||||||
|
| File | What it tests |
|
||||||
|
|---|---|
|
||||||
|
| `shares.service.spec.ts` | SharesService |
|
||||||
|
| `bonds.service.spec.ts` | BondsService |
|
||||||
|
| `candles.service.spec.ts` | CandlesService |
|
||||||
|
| `securities.controller.spec.ts` | SecuritiesController |
|
||||||
|
| `securities.service.spec.ts` | SecuritiesService |
|
||||||
|
| `moex-client.service.spec.ts` | MoexClientService |
|
||||||
|
|
||||||
|
## Frontend Tests
|
||||||
|
|
||||||
|
Фронтенд-тесты отсутствуют.
|
||||||
45
apps/docs/docs/frontend/api-client.md
Normal file
45
apps/docs/docs/frontend/api-client.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# API Client
|
||||||
|
|
||||||
|
## Client (`api/client.ts`)
|
||||||
|
|
||||||
|
Использует нативный `fetch` с единой обёрткой `request<T>`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function request<T>(
|
||||||
|
path: string,
|
||||||
|
params?: Record<string, string>,
|
||||||
|
): Promise<{ data: T; meta: ApiResponseMeta }>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Формирует URL из `path` + query params
|
||||||
|
- Парсит JSON-ответ в `ApiEnvelope<{ data: T, meta: ApiResponseMeta }>`
|
||||||
|
- Выбрасывает `Error` при HTTP-ошибке
|
||||||
|
|
||||||
|
## API Functions
|
||||||
|
|
||||||
|
| Function | Method | Path |
|
||||||
|
|---|---|---|
|
||||||
|
| `getHealth()` | GET | `/api/v1/health` |
|
||||||
|
| `searchSecurities(q, type?, limit?)` | GET | `/api/v1/securities/search` |
|
||||||
|
| `getShare(secid)` | GET | `/api/v1/securities/shares/:secid` |
|
||||||
|
| `getShareMarketData(secid)` | GET | `/api/v1/securities/shares/:secid/marketdata` |
|
||||||
|
| `getShareDividends(secid)` | GET | `/api/v1/securities/shares/:secid/dividends` |
|
||||||
|
| `getShareHistory(secid, from, till)` | GET | `/api/v1/securities/shares/:secid/history` |
|
||||||
|
| `getBond(secid)` | GET | `/api/v1/securities/bonds/:secid` |
|
||||||
|
| `getBondMarketData(secid)` | GET | `/api/v1/securities/bonds/:secid/marketdata` |
|
||||||
|
| `getBondHistory(secid, from, till)` | GET | `/api/v1/securities/bonds/:secid/history` |
|
||||||
|
| `getShareCandles(secid, interval, from, till)` | GET | `/api/v1/securities/shares/:secid/candles` |
|
||||||
|
| `getBondCandles(secid, interval, from, till)` | GET | `/api/v1/securities/bonds/:secid/candles` |
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
Ручные типы ответов в `api/responses.ts`:
|
||||||
|
|
||||||
|
- `ApiResponseMeta` — `{ cachedAt, fromCache }`
|
||||||
|
- `ApiEnvelope<T>` — `{ data: T, meta: ApiResponseMeta }`
|
||||||
|
- `ShareResponse` — спецификация акции + `StockMarketData`
|
||||||
|
- `BondResponse` — спецификация облигации + `BondMarketData`
|
||||||
|
- `CandleItem`, `DividendItem`, `ShareHistoryItem`, `BondHistoryItem`
|
||||||
|
- `SearchResultItem`, `HealthResponse`
|
||||||
|
|
||||||
|
Codegen-типы из OpenAPI в `api/types.ts` (генерируются через `npm run codegen`).
|
||||||
40
apps/docs/docs/frontend/components.md
Normal file
40
apps/docs/docs/frontend/components.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Components
|
||||||
|
|
||||||
|
## Layout (`components/Layout.tsx`)
|
||||||
|
|
||||||
|
Базовый layout с шапкой и `<Outlet />`.
|
||||||
|
|
||||||
|
- Шапка: логотип "MoexVibe" (ссылка на `/`) + `SearchBar`
|
||||||
|
- Основной контент: max-width 1200px, padding 24px
|
||||||
|
|
||||||
|
## SearchBar (`components/SearchBar.tsx`)
|
||||||
|
|
||||||
|
Поиск инструментов с debounce (300ms).
|
||||||
|
|
||||||
|
- Вызывает `useSearch(debounced)` при вводе 2+ символов
|
||||||
|
- Показывает выпадающий список результатов
|
||||||
|
- При клике на результат переходит на `/stocks/:secid` или `/bonds/:secid`
|
||||||
|
- Закрывается при клике вне компонента
|
||||||
|
|
||||||
|
## PriceChart (`components/PriceChart.tsx`)
|
||||||
|
|
||||||
|
График цены на основе `lightweight-charts` v4.
|
||||||
|
|
||||||
|
- Принимает массив свечей: `{ open, high, low, close, begin }`
|
||||||
|
- Высота по умолчанию: 400px
|
||||||
|
- Цвета: зелёный для роста, красный для падения
|
||||||
|
- Адаптивная ширина (resize listener)
|
||||||
|
|
||||||
|
## StockDetails (`components/StockDetails.tsx`)
|
||||||
|
|
||||||
|
Карточка с информацией об акции.
|
||||||
|
|
||||||
|
- Принимает `ShareResponse`
|
||||||
|
- Отображает: название, тикер, ISIN, цена, изменение (%), open/high/low, объём, капитализация, уровень листинга
|
||||||
|
|
||||||
|
## BondDetails (`components/BondDetails.tsx`)
|
||||||
|
|
||||||
|
Карточка с информацией об облигации.
|
||||||
|
|
||||||
|
- Принимает `BondResponse`
|
||||||
|
- Отображает: название, ISIN, цена (в % от номинала), номинал, дата погашения, купон (сумма/%), период купона, НКД, доходность к погашению, дюрация, тип
|
||||||
47
apps/docs/docs/frontend/hooks.md
Normal file
47
apps/docs/docs/frontend/hooks.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Hooks
|
||||||
|
|
||||||
|
Все хуки используют TanStack Query v5.
|
||||||
|
|
||||||
|
| Hook | Query Key | Stale Time | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `useSearch(query)` | `['securities', 'search', query]` | 60s | Поиск инструментов (enabled: query ≥ 2 символов) |
|
||||||
|
| `useStock(secid)` | `['stock', secid]` | 900s | Спецификация акции |
|
||||||
|
| `useStockCandles(secid, interval, from, till)` | `['stockCandles', secid, interval, from, till]` | 3600s | Свечи акции |
|
||||||
|
| `useStockDividends(secid)` | `['stockDividends', secid]` | 86400s | Дивиденды акции |
|
||||||
|
| `useBond(secid)` | `['bond', secid]` | 900s | Спецификация облигации |
|
||||||
|
| `useBondCandles(secid, interval, from, till)` | `['bondCandles', secid, interval, from, till]` | 3600s | Свечи облигации |
|
||||||
|
|
||||||
|
## Query Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 900_000, // 15 min
|
||||||
|
retry: 2,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hook Pattern
|
||||||
|
|
||||||
|
Каждый хук:
|
||||||
|
|
||||||
|
1. Вызывает функцию из `api/client.ts`
|
||||||
|
2. Извлекает `res.data` (ответ MOEX обёрнут в `{ data, meta }`)
|
||||||
|
3. Типизирован через рукописные типы из `api/responses.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useStock(secid: string) {
|
||||||
|
return useQuery<ShareResponse>({
|
||||||
|
queryKey: ['stock', secid],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getShare(secid);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 900_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
47
apps/docs/docs/frontend/overview.md
Normal file
47
apps/docs/docs/frontend/overview.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Frontend Overview
|
||||||
|
|
||||||
|
React SPA, собранная с Vite.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React 18
|
||||||
|
- react-router-dom v6
|
||||||
|
- TanStack Query v5
|
||||||
|
- lightweight-charts v4
|
||||||
|
- openapi-fetch (с рукописными типами `responses.ts`)
|
||||||
|
- Vite 5
|
||||||
|
|
||||||
|
## Source Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/frontend/src/
|
||||||
|
├── main.tsx # Entry point
|
||||||
|
├── App.tsx # BrowserRouter
|
||||||
|
├── routes.tsx # Маршруты
|
||||||
|
├── api/
|
||||||
|
│ ├── client.ts # HTTP-клиент (fetch)
|
||||||
|
│ ├── responses.ts # Типы ответов (ручные)
|
||||||
|
│ └── types.ts # Типы из openapi-typescript
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useSearch.ts
|
||||||
|
│ ├── useStock.ts
|
||||||
|
│ ├── useStockCandles.ts
|
||||||
|
│ ├── useStockDividends.ts
|
||||||
|
│ ├── useBond.ts
|
||||||
|
│ └── useBondCandles.ts
|
||||||
|
├── components/
|
||||||
|
│ ├── Layout.tsx
|
||||||
|
│ ├── SearchBar.tsx
|
||||||
|
│ ├── PriceChart.tsx
|
||||||
|
│ ├── StockDetails.tsx
|
||||||
|
│ └── BondDetails.tsx
|
||||||
|
├── pages/
|
||||||
|
│ ├── HomePage.tsx
|
||||||
|
│ ├── StockPage.tsx
|
||||||
|
│ └── BondPage.tsx
|
||||||
|
└── styles.css
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entry Point
|
||||||
|
|
||||||
|
`main.tsx` рендерит `App.tsx`, который содержит `BrowserRouter` → `AppRoutes`.
|
||||||
27
apps/docs/docs/frontend/routes.md
Normal file
27
apps/docs/docs/frontend/routes.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Routes
|
||||||
|
|
||||||
|
Определены в `apps/frontend/src/routes.tsx`.
|
||||||
|
|
||||||
|
| Path | Component | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `/` | `HomePage` | Главная страница с приветствием |
|
||||||
|
| `/stocks/:secid` | `StockPage` | Страница акции |
|
||||||
|
| `/bonds/:secid` | `BondPage` | Страница облигации |
|
||||||
|
|
||||||
|
Все страницы обёрнуты в `Layout`, который содержит:
|
||||||
|
|
||||||
|
- Шапку с логотипом (ссылка на `/`) и `SearchBar`
|
||||||
|
- `<main>` с максимальной шириной 1200px
|
||||||
|
- `<Outlet />` для контента страницы
|
||||||
|
|
||||||
|
## Route Structure
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Routes>
|
||||||
|
<Route element={<Layout />}>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/stocks/:secid" element={<StockPage />} />
|
||||||
|
<Route path="/bonds/:secid" element={<BondPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
```
|
||||||
17
apps/docs/docs/frontend/styling.md
Normal file
17
apps/docs/docs/frontend/styling.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Styling
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind.
|
||||||
|
|
||||||
|
## CSS Custom Properties
|
||||||
|
|
||||||
|
Определены в `styles.css`:
|
||||||
|
|
||||||
|
- `--color-surface` — фон карточек
|
||||||
|
- `--color-text` — основной цвет текста
|
||||||
|
- `--color-text-secondary` — второстепенный цвет текста
|
||||||
|
- `--color-positive` — положительное изменение (#2e7d32)
|
||||||
|
- `--color-negative` — отрицательное изменение (#c62828)
|
||||||
|
- `--border-radius` — радиус скругления
|
||||||
|
- `--shadow` — тень для карточек
|
||||||
43
apps/docs/docs/getting-started.md
Normal file
43
apps/docs/docs/getting-started.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- npm 9+
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone repository
|
||||||
|
git clone <repo-url>
|
||||||
|
cd moex-vibe
|
||||||
|
|
||||||
|
# 2. Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 3. Start backend (http://localhost:3000)
|
||||||
|
npm run dev:backend
|
||||||
|
|
||||||
|
# 4. In another terminal — start frontend (http://localhost:5173)
|
||||||
|
npm run dev:frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend dev-сервер проксирует `/api` → `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
- Frontend: http://localhost:80
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
1. Страница с поисковой строкой и заголовком "MoexVibe"
|
||||||
|
2. Swagger UI на http://localhost:3000/api/docs со всеми endpoint'ами
|
||||||
|
3. `curl http://localhost:3000/api/v1/health` возвращает `{ "status": "ok", ... }`
|
||||||
42
apps/docs/docs/infrastructure/ci.md
Normal file
42
apps/docs/docs/infrastructure/ci.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# CI/CD
|
||||||
|
|
||||||
|
## CI Pipeline
|
||||||
|
|
||||||
|
Определён в `.gitea/workflows/ci.yml`. Запускается на push/PR в `main`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Jobs
|
||||||
|
|
||||||
|
### lint
|
||||||
|
|
||||||
|
- `actions/checkout@v4`
|
||||||
|
- `actions/setup-node@v4` (Node 20)
|
||||||
|
- `npm ci`
|
||||||
|
- `npm run lint` (ESLint для бэкенда)
|
||||||
|
- `npx prettier --check "**/*.{ts,tsx}"` (проверка форматирования)
|
||||||
|
|
||||||
|
### test
|
||||||
|
|
||||||
|
- `actions/checkout@v4`
|
||||||
|
- `actions/setup-node@v4` (Node 20)
|
||||||
|
- `npm ci`
|
||||||
|
- `npm run test:backend` (vitest)
|
||||||
|
|
||||||
|
### build
|
||||||
|
|
||||||
|
- `actions/checkout@v4`
|
||||||
|
- `actions/setup-node@v4` (Node 20)
|
||||||
|
- `npm ci`
|
||||||
|
- `npm run build:backend` (nest build)
|
||||||
|
- `npm run build:frontend` (tsc -b && vite build)
|
||||||
112
apps/docs/docs/infrastructure/docker.md
Normal file
112
apps/docs/docs/infrastructure/docker.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
User["User"]
|
||||||
|
Nginx["Nginx :80"]
|
||||||
|
Backend["Node.js :3000"]
|
||||||
|
MOEX["MOEX ISS<br/>iss.moex.com"]
|
||||||
|
|
||||||
|
User -->|"HTTP"| Nginx
|
||||||
|
Nginx -->|"/api/* proxy_pass"| Backend
|
||||||
|
Nginx -->|"/* static files"| Nginx
|
||||||
|
Backend -->|"API calls"| MOEX
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
### Backend (`Dockerfile.backend`)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json tsconfig.base.json ./
|
||||||
|
COPY apps/backend/ apps/backend/
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build -w apps/backend
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/apps/backend/dist ./dist
|
||||||
|
COPY --from=build /app/apps/backend/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/apps/backend/package.json ./
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-stage build: сборка → production-образ с минимальными слоями.
|
||||||
|
|
||||||
|
### Frontend (`Dockerfile.frontend`)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json tsconfig.base.json ./
|
||||||
|
COPY apps/frontend/ apps/frontend/
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build -w apps/frontend
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Собирает статику, затем раздаёт через Nginx.
|
||||||
|
|
||||||
|
### Nginx Config (`nginx.conf`)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `/api/*` проксируется на backend-сервис
|
||||||
|
- Все остальные пути отдают SPA (index.html для клиентских маршрутов)
|
||||||
|
|
||||||
|
## Docker Compose (`docker-compose.yml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.backend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- PORT=3000
|
||||||
|
- MOEX_BASE_URL=https://iss.moex.com/iss
|
||||||
|
- MOEX_RATE_LIMIT=10
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.frontend
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
36
apps/docs/docs/intro.md
Normal file
36
apps/docs/docs/intro.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# MoexVibe
|
||||||
|
|
||||||
|
Веб-приложение для анализа ценных бумаг Московской биржи (MOEX).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend:** NestJS, TypeScript, OpenAPI (Swagger)
|
||||||
|
- **Frontend:** React 18, TypeScript, Vite, TanStack Query v5, lightweight-charts v4
|
||||||
|
- **Infrastructure:** Docker, docker-compose
|
||||||
|
- **CI:** Gitea Actions
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
moex-vibe/
|
||||||
|
├── apps/
|
||||||
|
│ ├── backend/ # NestJS API (единственная точка доступа к MOEX)
|
||||||
|
│ └── frontend/ # React SPA
|
||||||
|
├── docs/
|
||||||
|
│ ├── architecture/ # ADR и диаграммы
|
||||||
|
│ ├── openapi/ # OpenAPI-спецификация
|
||||||
|
│ └── superpowers/ # Дизайн-спеки и планы
|
||||||
|
├── docker/
|
||||||
|
│ ├── Dockerfile.backend
|
||||||
|
│ ├── Dockerfile.frontend
|
||||||
|
│ └── nginx.conf
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── tsconfig.base.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
- Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую.
|
||||||
|
- npm workspaces монорепозиторий: `apps/backend` и `apps/frontend`.
|
||||||
|
- Глобальный префикс API: `/api/v1`. Swagger: `/api/docs`.
|
||||||
|
- Ответы API обёрнуты в `{ data: T, meta: { fromCache, cachedAt } }`.
|
||||||
65
apps/docs/docusaurus.config.ts
Normal file
65
apps/docs/docusaurus.config.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { themes as prismThemes } from 'prism-react-renderer';
|
||||||
|
import type { Config } from '@docusaurus/types';
|
||||||
|
import type * as Preset from '@docusaurus/preset-classic';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
title: 'MoexVibe',
|
||||||
|
tagline: 'Analysis of MOEX securities',
|
||||||
|
favicon: 'img/favicon.ico',
|
||||||
|
|
||||||
|
url: 'https://moexvibe.example.com',
|
||||||
|
baseUrl: '/',
|
||||||
|
|
||||||
|
onBrokenLinks: 'warn',
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
locales: ['ru'],
|
||||||
|
},
|
||||||
|
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'classic',
|
||||||
|
{
|
||||||
|
docs: {
|
||||||
|
routeBasePath: '/',
|
||||||
|
sidebarPath: './sidebars.ts',
|
||||||
|
editUrl: undefined,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
customCss: './src/css/custom.css',
|
||||||
|
},
|
||||||
|
} satisfies Preset.Options,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
navbar: {
|
||||||
|
title: 'MoexVibe',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'docSidebar',
|
||||||
|
sidebarId: 'docsSidebar',
|
||||||
|
position: 'left',
|
||||||
|
label: 'Документация',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://github.com/ksv741/moex-vibe',
|
||||||
|
label: 'GitHub',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
style: 'dark',
|
||||||
|
copyright: `Copyright © ${new Date().getFullYear()} MoexVibe`,
|
||||||
|
},
|
||||||
|
prism: {
|
||||||
|
theme: prismThemes.github,
|
||||||
|
darkTheme: prismThemes.dracula,
|
||||||
|
},
|
||||||
|
} satisfies Preset.ThemeConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
21
apps/docs/package.json
Normal file
21
apps/docs/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@moex-vibe/docs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "docusaurus start",
|
||||||
|
"build": "docusaurus build",
|
||||||
|
"serve": "docusaurus serve"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.7.0",
|
||||||
|
"@docusaurus/preset-classic": "3.7.0",
|
||||||
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/docs/sidebars.ts
Normal file
64
apps/docs/sidebars.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
|
const sidebars: SidebarsConfig = {
|
||||||
|
docsSidebar: [
|
||||||
|
'intro',
|
||||||
|
'getting-started',
|
||||||
|
'architecture',
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Backend',
|
||||||
|
items: [
|
||||||
|
'backend/overview',
|
||||||
|
'backend/modules',
|
||||||
|
'backend/api',
|
||||||
|
'backend/configuration',
|
||||||
|
'backend/caching',
|
||||||
|
'backend/moex-client',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Frontend',
|
||||||
|
items: [
|
||||||
|
'frontend/overview',
|
||||||
|
'frontend/routes',
|
||||||
|
'frontend/components',
|
||||||
|
'frontend/hooks',
|
||||||
|
'frontend/api-client',
|
||||||
|
'frontend/styling',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Infrastructure',
|
||||||
|
items: ['infrastructure/docker', 'infrastructure/ci'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Development',
|
||||||
|
items: [
|
||||||
|
'development/commands',
|
||||||
|
'development/testing',
|
||||||
|
'development/codegen',
|
||||||
|
'development/conventions',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Architecture Decisions (ADR)',
|
||||||
|
items: [
|
||||||
|
'adr/index',
|
||||||
|
'adr/ADR-001-backend-single-point-of-access',
|
||||||
|
'adr/ADR-002-in-memory-cache',
|
||||||
|
'adr/ADR-003-rate-limiting-strategy',
|
||||||
|
'adr/ADR-004-feature-modules',
|
||||||
|
'adr/ADR-005-openapi-codegen-frontend',
|
||||||
|
'adr/ADR-006-no-cci',
|
||||||
|
'adr/ADR-007-two-level-caching',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sidebars;
|
||||||
13
apps/docs/src/css/custom.css
Normal file
13
apps/docs/src/css/custom.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
:root {
|
||||||
|
--ifm-color-primary: #1976d2;
|
||||||
|
--ifm-color-primary-dark: #1565c0;
|
||||||
|
--ifm-color-primary-darker: #0d47a1;
|
||||||
|
--ifm-color-primary-light: #2196f3;
|
||||||
|
--ifm-color-primary-lighter: #42a5f5;
|
||||||
|
--ifm-code-font-size: 95%;
|
||||||
|
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
47
docs/superpowers/specs/2026-06-13-docusaurus-docs-design.md
Normal file
47
docs/superpowers/specs/2026-06-13-docusaurus-docs-design.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Docusaurus Documentation — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Создать Docusaurus-сайт для onboard'инга новых разработчиков за 15 минут и документирования архитектуры MoexVibe.
|
||||||
|
|
||||||
|
## Location
|
||||||
|
|
||||||
|
`apps/docs/` — npm workspace в монорепозитории (по аналогии с `apps/backend`, `apps/frontend`).
|
||||||
|
|
||||||
|
## Content Sources (только из кода, без вымысла)
|
||||||
|
|
||||||
|
- `package.json` корневой + воркспейсов → scripts, deps
|
||||||
|
- `AGENTS.md` → env vars, architecture description, commands table
|
||||||
|
- `apps/backend/src/**` → модули, контроллеры, сервисы, DTO, конфиг
|
||||||
|
- `apps/frontend/src/**` → компоненты, хуки, клиент, типы
|
||||||
|
- `docker/Dockerfile.*`, `docker/nginx.conf` → инфраструктура
|
||||||
|
- `docker-compose.yml` → сервисы, порты
|
||||||
|
- `.gitea/workflows/ci.yml` → CI pipeline
|
||||||
|
- `docs/architecture/adr/*` → копия существующих ADR
|
||||||
|
- `.prettierrc`, `tsconfig.base.json` → конфиги
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
| Раздел | Страницы |
|
||||||
|
|---|---|
|
||||||
|
| Overview | intro, getting-started, architecture |
|
||||||
|
| Backend | overview, modules, api, configuration, caching, moex-client |
|
||||||
|
| Frontend | overview, routes, components, hooks, api-client, styling |
|
||||||
|
| Infrastructure | docker, ci |
|
||||||
|
| Development | commands, testing, codegen, conventions |
|
||||||
|
| ADR | index + 7 ADR-документов (копия) |
|
||||||
|
|
||||||
|
## Mermaid Diagrams
|
||||||
|
|
||||||
|
1. **architecture** — `graph TD` (Browser → SPA → Backend → Cache → MOEX)
|
||||||
|
2. **architecture** — `sequenceDiagram` (cache hit/miss flow)
|
||||||
|
3. **backend/modules** — `graph TD` (module dependency graph)
|
||||||
|
4. **backend/caching** — `flowchart LR` (getOrFetch lifecycle)
|
||||||
|
5. **infrastructure/docker** — `graph TD` (deployment)
|
||||||
|
|
||||||
|
## Root Changes
|
||||||
|
|
||||||
|
- Добавить `apps/docs` в `workspaces[]` корневого `package.json`
|
||||||
|
- Добавить скрипты `dev:docs` и `build:docs`
|
||||||
15480
package-lock.json
generated
15480
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/backend",
|
"apps/backend",
|
||||||
"apps/frontend"
|
"apps/frontend",
|
||||||
|
"apps/docs"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:backend": "npm run start:dev -w apps/backend",
|
"dev:backend": "npm run start:dev -w apps/backend",
|
||||||
@ -13,7 +14,9 @@
|
|||||||
"test:backend": "npm run test -w apps/backend",
|
"test:backend": "npm run test -w apps/backend",
|
||||||
"lint": "npm run lint -w apps/backend",
|
"lint": "npm run lint -w apps/backend",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx}\""
|
"format:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||||
|
"dev:docs": "npm run dev -w apps/docs",
|
||||||
|
"build:docs": "npm run build -w apps/docs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.0.0"
|
"prettier": "^3.0.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user