docs: add Docusaurus documentation site as npm workspace
All checks were successful
CI / lint (pull_request) Successful in 1m32s
CI / test (pull_request) Successful in 1m23s
CI / build (pull_request) Successful in 1m27s
CI / lint (push) Successful in 1m16s
CI / test (push) Successful in 1m23s
CI / build (push) Successful in 1m32s

- 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:
Sergey Krylov 2026-06-13 21:35:20 +03:00
parent a6d4b25afe
commit aac5b2f873
38 changed files with 16999 additions and 272 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ dist/
*.tsbuildinfo
vite.config.d.ts
vite.config.js
apps/docs/.docusaurus/
apps/docs/build/

View File

@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View 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), но нивелируется кешированием

View 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)

View 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)

View 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)

View 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

View 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) в первой версии

View 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)

View 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/`.

View 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

View 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 }
}
```

View 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` |

View 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),
},
}));
```

View 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

View 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` — торговая доска

View 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/
```

View 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'овым и поддерживаются вручную.

View 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
```

View 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 и кеширование

View 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
Фронтенд-тесты отсутствуют.

View 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`).

View 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, цена (в % от номинала), номинал, дата погашения, купон (сумма/%), период купона, НКД, доходность к погашению, дюрация, тип

View 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,
});
}
```

View 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`.

View 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>
```

View 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` — тень для карточек

View 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", ... }`

View 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)

View 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
View 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 } }`.

View 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
View 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
View 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;

View 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);
}

View 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

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,8 @@
"private": true,
"workspaces": [
"apps/backend",
"apps/frontend"
"apps/frontend",
"apps/docs"
],
"scripts": {
"dev:backend": "npm run start:dev -w apps/backend",
@ -13,7 +14,9 @@
"test:backend": "npm run test -w apps/backend",
"lint": "npm run lint -w apps/backend",
"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": {
"prettier": "^3.0.0"