create specification and plan for MVP

This commit is contained in:
Sergey Krylov 2026-06-13 18:42:46 +03:00
commit e4ec1eea0a
11 changed files with 4958 additions and 0 deletions

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)

776
docs/openapi/openapi.yaml Normal file
View File

@ -0,0 +1,776 @@
openapi: "3.0.3"
info:
title: MoexVibe API
description: |
API для анализа ценных бумаг Московской биржи.
Backend является единственной точкой доступа к MOEX ISS.
version: "1.0.0"
contact:
name: MoexVibe Team
servers:
- url: http://localhost:3000/api/v1
description: Local development
- url: https://api.moexvibe.example.com/api/v1
description: Production
paths:
/health:
get:
operationId: healthCheck
tags: [Health]
summary: Проверка состояния сервиса
responses:
"200":
description: Сервис работает
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
/securities/search:
get:
operationId: searchSecurities
tags: [Securities]
summary: Поиск по инструментам
parameters:
- name: q
in: query
required: true
schema:
type: string
minLength: 1
maxLength: 100
description: Поисковый запрос (тикер, название, ISIN)
- name: type
in: query
required: false
schema:
type: string
enum: [all, share, bond]
default: all
description: Фильтр по типу инструмента
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: Максимальное количество результатов
responses:
"200":
description: Результаты поиска
content:
application/json:
schema:
$ref: "#/components/schemas/SearchResponse"
"400":
$ref: "#/components/responses/BadRequest"
/securities/shares/{secid}:
get:
operationId: getShare
tags: [Shares]
summary: Получить спецификацию акции
parameters:
- name: secid
in: path
required: true
schema:
type: string
description: SECID инструмента (e.g. SBER)
responses:
"200":
description: Спецификация акции
content:
application/json:
schema:
$ref: "#/components/schemas/StockResponse"
"404":
$ref: "#/components/responses/NotFound"
/securities/shares/{secid}/marketdata:
get:
operationId: getShareMarketData
tags: [Shares]
summary: Получить рыночные данные акции
parameters:
- name: secid
in: path
required: true
schema:
type: string
responses:
"200":
description: Рыночные данные
content:
application/json:
schema:
$ref: "#/components/schemas/StockMarketDataResponse"
"404":
$ref: "#/components/responses/NotFound"
/securities/shares/{secid}/candles:
get:
operationId: getShareCandles
tags: [Shares]
summary: Получить свечи для графика цены акции
parameters:
- name: secid
in: path
required: true
schema:
type: string
- name: interval
in: query
required: true
schema:
type: string
enum: ["1h", "24h"]
description: Таймфрейм свечей
- name: from
in: query
required: true
schema:
type: string
format: date
description: Начальная дата (ISO 8601)
- name: till
in: query
required: true
schema:
type: string
format: date
description: Конечная дата (ISO 8601)
responses:
"200":
description: Массив свечей
content:
application/json:
schema:
$ref: "#/components/schemas/CandlesResponse"
"400":
$ref: "#/components/responses/BadRequest"
/securities/shares/{secid}/history:
get:
operationId: getShareHistory
tags: [Shares]
summary: Получить дневную историю торгов акции
parameters:
- name: secid
in: path
required: true
schema:
type: string
- name: from
in: query
required: true
schema:
type: string
format: date
- name: till
in: query
required: true
schema:
type: string
format: date
responses:
"200":
description: Дневная история
content:
application/json:
schema:
$ref: "#/components/schemas/HistoryResponse"
/securities/shares/{secid}/dividends:
get:
operationId: getShareDividends
tags: [Shares]
summary: Получить историю дивидендных выплат
parameters:
- name: secid
in: path
required: true
schema:
type: string
responses:
"200":
description: Дивиденды
content:
application/json:
schema:
$ref: "#/components/schemas/DividendsResponse"
"404":
$ref: "#/components/responses/NotFound"
/securities/bonds/{secid}:
get:
operationId: getBond
tags: [Bonds]
summary: Получить спецификацию облигации
parameters:
- name: secid
in: path
required: true
schema:
type: string
responses:
"200":
description: Спецификация облигации
content:
application/json:
schema:
$ref: "#/components/schemas/BondResponse"
"404":
$ref: "#/components/responses/NotFound"
/securities/bonds/{secid}/marketdata:
get:
operationId: getBondMarketData
tags: [Bonds]
summary: Получить рыночные данные облигации
parameters:
- name: secid
in: path
required: true
schema:
type: string
responses:
"200":
description: Рыночные данные облигации
content:
application/json:
schema:
$ref: "#/components/schemas/BondMarketDataResponse"
"404":
$ref: "#/components/responses/NotFound"
/securities/bonds/{secid}/candles:
get:
operationId: getBondCandles
tags: [Bonds]
summary: Получить свечи для графика цены облигации
parameters:
- name: secid
in: path
required: true
schema:
type: string
- name: interval
in: query
required: true
schema:
type: string
enum: ["1h", "24h"]
- name: from
in: query
required: true
schema:
type: string
format: date
- name: till
in: query
required: true
schema:
type: string
format: date
responses:
"200":
description: Массив свечей
content:
application/json:
schema:
$ref: "#/components/schemas/CandlesResponse"
/securities/bonds/{secid}/history:
get:
operationId: getBondHistory
tags: [Bonds]
summary: Получить дневную историю торгов облигации
parameters:
- name: secid
in: path
required: true
schema:
type: string
- name: from
in: query
required: true
schema:
type: string
format: date
- name: till
in: query
required: true
schema:
type: string
format: date
responses:
"200":
description: Дневная история
content:
application/json:
schema:
$ref: "#/components/schemas/BondHistoryResponse"
components:
schemas:
# ── Health ──
HealthResponse:
type: object
properties:
status:
type: string
example: "ok"
timestamp:
type: string
format: date-time
uptime:
type: number
required: [status, timestamp, uptime]
# ── Api Response Wrapper ──
ApiResponse:
type: object
properties:
data: {}
meta:
type: object
properties:
cachedAt:
type: string
format: date-time
nullable: true
fromCache:
type: boolean
required: [data]
# ── Search ──
SearchResult:
type: object
properties:
secid:
type: string
example: "SBER"
isin:
type: string
example: "RU0009029540"
shortName:
type: string
example: "Сбербанк"
type:
type: string
enum: [share, bond]
listLevel:
type: integer
example: 1
currency:
type: string
nullable: true
example: "RUB"
price:
type: number
nullable: true
example: 322.35
required: [secid, isin, shortName, type, listLevel]
SearchResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/SearchResult"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
# ── Stock ──
StockMarketData:
type: object
properties:
price:
type: number
example: 322.35
change:
type: number
example: 1.15
changePercent:
type: number
example: 0.36
open:
type: number
example: 321.30
high:
type: number
example: 322.66
low:
type: number
nullable: true
example: 321.20
volume:
type: integer
example: 1925163
value:
type: number
example: 620184479
issueCapitalization:
type: number
example: 6958336818320
updatedAt:
type: string
format: date-time
example: "2026-06-13T18:03:11Z"
required: [price, change, changePercent, open, volume, value, updatedAt]
Stock:
type: object
properties:
secid:
type: string
example: "SBER"
isin:
type: string
example: "RU0009029540"
name:
type: string
example: "Сбербанк России ПАО ао"
shortName:
type: string
example: "Сбербанк"
latName:
type: string
nullable: true
example: "Sberbank"
listLevel:
type: integer
example: 1
issueSize:
type: integer
example: 21586948000
faceValue:
type: number
example: 3
faceUnit:
type: string
example: "RUB"
type:
type: string
example: "common_share"
marketData:
$ref: "#/components/schemas/StockMarketData"
required: [secid, isin, name, shortName, listLevel, type, marketData]
StockResponse:
type: object
properties:
data:
$ref: "#/components/schemas/Stock"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
StockMarketDataResponse:
type: object
properties:
data:
$ref: "#/components/schemas/StockMarketData"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
# ── Bond ──
BondMarketData:
type: object
properties:
price:
type: number
example: 100.45
description: Цена в % от номинала
yieldToMaturity:
type: number
nullable: true
example: 12.71
yieldAtWaprice:
type: number
nullable: true
duration:
type: number
nullable: true
accruedInt:
type: number
example: 29.48
couponValue:
type: number
example: 40.64
couponPercent:
type: number
nullable: true
example: 8.15
nextCouponDate:
type: string
format: date
nullable: true
example: "2026-08-05"
open:
type: number
high:
type: number
nullable: true
low:
type: number
nullable: true
volume:
type: integer
updatedAt:
type: string
format: date-time
required: [price, accruedInt, couponValue, volume, updatedAt]
Bond:
type: object
properties:
secid:
type: string
isin:
type: string
name:
type: string
shortName:
type: string
latName:
type: string
nullable: true
listLevel:
type: integer
issueSize:
type: integer
faceValue:
type: number
faceUnit:
type: string
matDate:
type: string
format: date
example: "2027-02-03"
couponValue:
type: number
example: 40.64
couponPercent:
type: number
nullable: true
example: 8.15
couponPeriod:
type: integer
example: 182
nextCoupon:
type: string
format: date
example: "2026-08-05"
accruedInt:
type: number
example: 29.48
bondType:
type: string
example: "Фикс с известным купоном"
bondSubType:
type: string
example: "До погашения"
offerDate:
type: string
format: date
nullable: true
buybackDate:
type: string
format: date
nullable: true
marketData:
$ref: "#/components/schemas/BondMarketData"
required: [secid, isin, name, shortName, listLevel, matDate, couponValue,
couponPeriod, accruedInt, bondType, marketData]
BondResponse:
type: object
properties:
data:
$ref: "#/components/schemas/Bond"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
BondMarketDataResponse:
type: object
properties:
data:
$ref: "#/components/schemas/BondMarketData"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
# ── Candle ──
Candle:
type: object
properties:
open:
type: number
example: 280.00
high:
type: number
example: 280.41
low:
type: number
example: 271.80
close:
type: number
example: 272.25
volume:
type: integer
example: 43086870
value:
type: number
example: 11853565984.9
begin:
type: string
format: date-time
example: "2025-01-03T00:00:00Z"
end:
type: string
format: date-time
example: "2025-01-03T23:59:59Z"
required: [open, high, low, close, volume, value, begin, end]
CandlesResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Candle"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
# ── History ──
HistoryEntry:
type: object
properties:
date:
type: string
format: date
open:
type: number
high:
type: number
low:
type: number
close:
type: number
volume:
type: integer
value:
type: number
required: [date, open, high, low, close, volume, value]
HistoryResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/HistoryEntry"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
BondHistoryEntry:
type: object
properties:
date:
type: string
format: date
closePrice:
type: number
yieldClose:
type: number
nullable: true
duration:
type: number
nullable: true
required: [date, closePrice]
BondHistoryResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/BondHistoryEntry"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
# ── Dividend ──
Dividend:
type: object
properties:
registryCloseDate:
type: string
format: date
example: "2025-07-18"
value:
type: number
example: 34.84
currency:
type: string
example: "RUB"
required: [registryCloseDate, value, currency]
DividendsResponse:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Dividend"
meta:
$ref: "#/components/schemas/ApiResponse/properties/meta"
# ── Error ──
ErrorResponse:
type: object
properties:
statusCode:
type: integer
example: 404
message:
type: string
example: "Instrument SBER_NOT_FOUND not found"
error:
type: string
example: "Not Found"
timestamp:
type: string
format: date-time
path:
type: string
example: "/api/v1/securities/shares/SBER_NOT_FOUND"
required: [statusCode, message, error, timestamp, path]
responses:
BadRequest:
description: Неверный запрос
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Инструмент не найден
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
tags:
- name: Health
description: Мониторинг состояния сервиса
- name: Securities
description: Поиск инструментов
- name: Shares
description: Акции
- name: Bonds
description: Облигации

193
docs/requirements.md Normal file
View File

@ -0,0 +1,193 @@
Ты выступаешь как Senior Solution Architect, Tech Lead и Product Analyst.
Нужно спроектировать MVP приложения для анализа инвестиций на Московской бирже (MOEX).
Перед составлением спецификации и плана разработки ты ОБЯЗАН выявить все недостающие требования и задать уточняющие вопросы. Не переходи к проектированию, пока все критические вопросы не будут закрыты.
## Источники данных
Использовать только официальные API и документацию MOEX:
* https://www.moex.com/a2193
* https://www.moex.com/a7939
* https://iss.moex.com/iss/reference/
Перед проектированием изучи доступные методы API и предложи оптимальную модель интеграции.
---
# Цель MVP
Разработать веб-приложение для анализа ценных бумаг Московской биржи.
## MVP должен включать
### Главная страница
* глобальный поиск по инструментам
* поиск акций
* поиск облигаций
* отображение результатов поиска
* переход на карточку инструмента
### Страница акции
Отображение:
* тикера
* названия компании
* текущей цены
* капитализации
* дивидендной информации
* доходности
* основных финансовых показателей (если доступны через MOEX)
* исторических данных
* графика цены
### Страница облигации
Отображение:
* ISIN
* тикера
* эмитента
* номинала
* купона
* даты погашения
* текущей цены
* доходности к погашению
* накопленного купонного дохода
* графика цены
* прочих доступных параметров
---
# Технологический стек
## Frontend
* React
* TypeScript
* Vite
* TanStack Query
* React Router
* OpenAPI Code Generation
* максимальная типизация
* SSR не требуется
## Backend
* NestJS
* TypeScript
* OpenAPI (Swagger)
* архитектура по feature modules
* DTO validation
* централизованная обработка ошибок
* structured logging
* request/response logging middleware
* healthcheck endpoint
* configuration module
## Документация
Использовать Docusaurus.
Документация должна включать:
* архитектурные решения (ADR)
* sequence diagrams
* component diagrams
* deployment diagrams
* API documentation
* OpenAPI схемы
* описание бизнес-процессов
* onboarding разработчиков
---
# Подход к разработке
Использовать:
* Superpowers
* OpenSpec
Разработка должна начинаться со спецификации.
Сначала сформировать:
1. Product Requirements Document (PRD)
2. Domain Model
3. Architecture Decision Records (ADR)
4. OpenAPI Contract
5. Frontend Architecture
6. Backend Architecture
7. План реализации по этапам
---
# Требования к API
Backend является единственной точкой доступа к MOEX.
Frontend не должен обращаться к MOEX напрямую.
Backend должен:
* агрегировать данные MOEX
* кешировать ответы
* нормализовать модели данных
* предоставлять собственный OpenAPI контракт
Необходимо предложить стратегию:
* кеширования
* rate limiting
* обработки ошибок MOEX
* обновления данных
---
# Требования к Frontend
Использовать OpenAPI codegen для генерации:
* API clients
* DTO
* React Query hooks (если возможно)
Не писать API-клиенты вручную без необходимости.
Предложить оптимальную структуру проекта.
---
# UX/UI
Использовать современные практики frontend разработки.
При проектировании интерфейсов:
* использовать MCP инструменты для анализа и генерации дизайна
* использовать frontend design skills
* подготовить описание экранов
* подготовить user flow
* подготовить wireframes в текстовом виде
---
# Ожидаемый результат
После уточнения требований сформируй:
1. список вопросов
2. PRD
3. OpenSpec спецификацию
4. архитектуру системы
5. структуру репозитория
6. OpenAPI проект
7. план реализации по спринтам
8. список рисков
9. roadmap развития после MVP
Не сокращай ответы. Действуй как архитектор уровня Staff+/Principal Engineer.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,338 @@
# MoexVibe — MVP Design Specification
**Date:** 2026-06-13
**Status:** Draft
**Author:** AI Assistant (Staff+ Architect)
---
## 1. Product Requirements Document (PRD)
### 1.1 Product Vision
Веб-приложение для анализа ценных бумаг Московской биржи (MOEX). Позволяет искать акции и облигации, просматривать их текущие параметры, доходность, дивиденды/купоны, историю торгов и графики цены.
### 1.2 Target Audience
Частные инвесторы, интересующиеся российским фондовым рынком. B2C, read-only сервис без аутентификации.
### 1.3 MVP Scope
| In Scope | Out of Scope |
|----------|-------------|
| Поиск по инструментам (акции + облигации) | Аутентификация / пользователи |
| Карточка акции (цена, капитализация, дивиденды, график) | Портфели и избранное |
| Карточка облигации (ISIN, купон, НКД, YTM, дюрация, график) | Сравнение инструментов |
| Часовые и дневные свечи (1 год истории) | Финансовая отчётность (МСФО/РСБУ) |
| Docker-ready деплой | Фьючерсы, опционы, валютный рынок |
| Документация (ADR, API, архитектура) | Real-time данные (WebSocket) |
| | Экспорт данных |
| | Мобильные приложения |
### 1.4 User Stories
- US-001: Пользователь вводит текст в поиск и видит подходящие акции и облигации
- US-002: Пользователь переходит на карточку акции, видит текущую цену, изменение, капитализацию
- US-003: Пользователь видит историю дивидендных выплат по акции
- US-004: Пользователь видит график цены (дневные и часовые свечи) за последний год
- US-005: Пользователь переходит на карточку облигации, видит ISIN, номинал, купон, дату погашения
- US-006: Пользователь видит НКД, доходность к погашению, дюрацию
- US-007: Пользователь видит график цены облигации за последний год
### 1.5 Non-Functional Requirements
- Максимальное время ответа API: < 500ms (p95) при попадании в кеш
- Доступность: бэкенд stateless, готов к масштабированию
- Задержка данных: 15 минут (бесплатный MOEX ISS)
- Все ответы API кешируются на бэкенде
---
## 2. Domain Model
```
Security (abstract base)
├── secid: string — "SBER"
├── isin: string — "RU0009029540"
├── name: string — полное наименование
├── shortName: string — краткое наименование
├── latName: string | null
├── listLevel: 1 | 2 | 3 — уровень листинга
├── issueSize: number — объём выпуска
├── faceValue: number — номинал
├── faceUnit: string — "RUB" / "USD" / "SUR"
├── issueDate: string — ISO date
├── isQualifiedInvestors: boolean
├── morningSession: boolean
├── eveningSession: boolean
├── Stock
│ ├── type: "common_share" | "preferred_share"
│ ├── marketData: StockMarketData
│ │ ├── price: number
│ │ ├── change: number
│ │ ├── changePercent: number
│ │ ├── open: number
│ │ ├── high: number
│ │ ├── low: number
│ │ ├── volume: number
│ │ ├── value: number
│ │ └── issueCapitalization: number
│ └── dividends: Dividend[]
│ ├── registryCloseDate: string (ISO date)
│ ├── value: number (RUB per share)
│ └── currency: string
└── Bond
├── matDate: string — дата погашения
├── couponValue: number — размер купона (RUB)
├── couponPercent: number|null — ставка купона (%)
├── couponPeriod: number — дней между купонами
├── nextCoupon: string (ISO date)
├── accruedInt: number — НКД
├── bondType: string — "Фикс" / "Флоатер" / "Линкер" / etc
├── bondSubType: string — "До погашения" / "До оферты"
├── offerDate: string | null
├── buybackDate: string | null
├── marketData: BondMarketData
│ ├── price: number — % от номинала
│ ├── yieldToMaturity: number | null
│ ├── duration: number | null
│ ├── open: number
│ ├── high: number | null
│ ├── low: number | null
│ └── volume: number
└── history: BondHistoryEntry[]
├── date: string
├── closePrice: number
├── yieldClose: number
└── duration: number
Candle
├── open: number
├── high: number
├── low: number
├── close: number
├── volume: number
├── value: number
├── begin: string (ISO datetime)
└── end: string (ISO datetime)
SearchResult
├── secid: string
├── isin: string
├── shortName: string
├── type: "share" | "bond"
├── listLevel: number
├── currency: string | null
└── price: number | null
```
---
## 3. Architecture
```
┌──────────────┐ ┌─────────────────────────────────────┐ ┌──────────────┐
│ Browser │────▶│ NestJS Backend │────▶│ MOEX ISS │
│ (React SPA) │◀────│ (1 instance, stateless) │◀────│ (HTTP) │
└──────────────┘ │ │ └──────────────┘
│ ┌─────────────────────────────────┐ │
│ │ Core Modules │ │
│ │ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Search │ │ SharesModule │ │ │
│ │ │ Module │ │ (stocks) │ │ │
│ │ └──────────┘ └───────────────┘ │ │
│ │ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Bonds │ │ Candles │ │ │
│ │ │ Module │ │ Module │ │ │
│ │ └──────────┘ └───────────────┘ │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Shared Infrastructure │ │
│ │ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ MOEX │ │ Cache │ │ │
│ │ │ Client │ │ Manager │ │ │
│ │ │(rate-ltd│ │ (in-memory) │ │ │
│ │ │ circuit │ │ │ │ │
│ │ │breaker) │ │ │ │ │
│ │ └──────────┘ └───────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
```
### 3.1 Caching Strategy
| Data Type | Backend TTL | Frontend staleTime | Notes |
|-----------|-------------|-------------------|-------|
| MarketData | 900s (15m) | 900s | Совпадает с задержкой MOEX |
| History | 3600s (1h) | 3600s | Обновляется раз в день после торгов |
| Candles | 3600s (1h) | 3600s | Дневные свечи не меняются intraday |
| Security spec | 86400s (1d) | 86400s | Редко меняется |
| Search results | 3600s (1h) | 3600s | |
| Dividends | 86400s (1d) | 86400s | |
### 3.2 Error Handling Strategy
- Все MOEX-ошибки маппятся в нормализованный `ErrorResponse`
- При пустых данных (выходные, праздники) — `200` с `null` значениями, не `404`
- Circuit breaker: при 5+ последовательных ошибках MOEX — пауза 30s
- Graceful degradation: если MOEX недоступен, возвращать последние кешированные данные
### 3.3 Rate Limiting
- MOEX Client: очередь запросов ~10 req/s (конфигурируется)
- При превышении — автоматическое ожидание в очереди
- Отсутствие внешнего rate limiter на уровне NestJS (приложение публичное, read-only)
---
## 4. API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/health` | Healthcheck |
| GET | `/api/v1/securities/search` | Поиск по инструментам |
| GET | `/api/v1/securities/shares/:secid` | Спецификация акции |
| GET | `/api/v1/securities/shares/:secid/marketdata` | Рыночные данные акции |
| GET | `/api/v1/securities/shares/:secid/candles` | Свечи (1h/24h) |
| GET | `/api/v1/securities/shares/:secid/history` | Дневная история |
| GET | `/api/v1/securities/shares/:secid/dividends` | Дивиденды |
| GET | `/api/v1/securities/bonds/:secid` | Спецификация облигации |
| GET | `/api/v1/securities/bonds/:secid/marketdata` | Рыночные данные облигации |
| GET | `/api/v1/securities/bonds/:secid/candles` | Свечи (1h/24h) |
| GET | `/api/v1/securities/bonds/:secid/history` | Дневная история |
---
## 5. Repo Structure
```
moex-vibe/
├── apps/
│ ├── backend/
│ │ ├── src/
│ │ │ ├── main.ts
│ │ │ ├── app.module.ts
│ │ │ ├── common/
│ │ │ │ ├── dto/
│ │ │ │ │ ├── api-response.dto.ts
│ │ │ │ │ └── pagination.dto.ts
│ │ │ │ ├── filters/
│ │ │ │ │ └── http-exception.filter.ts
│ │ │ │ ├── interceptors/
│ │ │ │ │ ├── logging.interceptor.ts
│ │ │ │ │ └── transform.interceptor.ts
│ │ │ │ └── middleware/
│ │ │ │ └── request-logging.middleware.ts
│ │ │ ├── config/
│ │ │ │ └── configuration.ts
│ │ │ └── modules/
│ │ │ ├── moex-client/
│ │ │ ├── cache/
│ │ │ ├── securities/
│ │ │ ├── shares/
│ │ │ ├── bonds/
│ │ │ └── health/
│ │ ├── test/
│ │ └── package.json
│ └── frontend/
│ ├── src/
│ │ ├── api/ # openapi-typescript generated
│ │ │ ├── types.ts
│ │ │ └── client.ts
│ │ ├── hooks/
│ │ │ ├── useStock.ts
│ │ │ ├── useBond.ts
│ │ │ ├── useSearch.ts
│ │ │ ├── useCandles.ts
│ │ │ └── useDividends.ts
│ │ ├── pages/
│ │ │ ├── HomePage.tsx
│ │ │ ├── StockPage.tsx
│ │ │ └── BondPage.tsx
│ │ ├── components/
│ │ │ ├── Layout/
│ │ │ ├── SearchBar/
│ │ │ ├── SecurityCard/
│ │ │ ├── PriceChart/
│ │ │ ├── StockDetails/
│ │ │ └── BondDetails/
│ │ ├── routes.tsx
│ │ └── main.tsx
│ └── package.json
├── docs/
│ ├── superpowers/specs/
│ ├── architecture/
│ │ ├── adr/
│ │ ├── diagrams/
│ │ └── domain-model.md
│ ├── openapi/
│ │ └── openapi.yaml
│ └── website/ (Docusaurus — post-MVP)
├── package.json
├── tsconfig.base.json
└── .gitignore
```
---
## 6. Sprint Plan
### Sprint 1 — Backend Foundation
- NestJS project init + npm workspaces
- ConfigurationModule, Logging, Global filters
- MoexClientModule (rate-limited HTTP client)
- CacheModule (cache-manager in-memory)
- HealthController
- ESLint, Prettier, tsconfig
### Sprint 2 — Securities API
- SecuritiesModule (search)
- SharesModule (spec + marketdata + dividends)
- OpenAPI decorators
- Unit tests
### Sprint 3 — Bonds + History
- BondsModule (spec + marketdata)
- CandlesModule (shares + bonds)
- HistoryModule
- OpenAPI decorators
- Unit tests
### Sprint 4 — Frontend Foundation
- Vite + React + TypeScript init
- openapi-typescript codegen
- TanStack Query + React Router
- Layout, SearchBar, HomePage
### Sprint 5 — Frontend Details
- StockPage (price block, dividends table, chart)
- BondPage (bond details, chart)
- PriceChart component (lightweight-charts)
- Loading/error states
### Sprint 6 — Docs + Infrastructure
- ADRs, architecture docs
- OpenAPI spec
- Dockerfile + docker-compose
- README
---
## 7. Risks
| Risk | Impact | Mitigation |
|------|--------|-----------|
| MOEX ISS API changes | High | MoexClient abstraction layer |
| Rate limiting by MOEX | Medium | p-queue + circuit breaker |
| Empty data on holidays/weekends | Low | Graceful null handling |
| Large search result sets | Low | Server-side limit + frontend debounce |
## 8. Post-MVP Roadmap
1. Финансовая отчётность (MOEX CCI — IFRS/RAS)
2. Аутентификация, портфели, избранное
3. Сравнение инструментов (multi-chart)
4. Фьючерсы и опционы
5. Экспорт (CSV, PDF)
6. WebSocket для real-time данных
7. Redis для масштабирования