create specification and plan for MVP
This commit is contained in:
commit
e4ec1eea0a
@ -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
docs/architecture/adr/ADR-002-in-memory-cache.md
Normal file
33
docs/architecture/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
docs/architecture/adr/ADR-003-rate-limiting-strategy.md
Normal file
21
docs/architecture/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
docs/architecture/adr/ADR-004-feature-modules.md
Normal file
29
docs/architecture/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
docs/architecture/adr/ADR-005-openapi-codegen-frontend.md
Normal file
40
docs/architecture/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
docs/architecture/adr/ADR-006-no-cci.md
Normal file
20
docs/architecture/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
docs/architecture/adr/ADR-007-two-level-caching.md
Normal file
27
docs/architecture/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)
|
||||
776
docs/openapi/openapi.yaml
Normal file
776
docs/openapi/openapi.yaml
Normal 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
193
docs/requirements.md
Normal 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.
|
||||
3456
docs/superpowers/plans/2026-06-13-moex-vibe-implementation.md
Normal file
3456
docs/superpowers/plans/2026-06-13-moex-vibe-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
338
docs/superpowers/specs/2026-06-13-moex-vibe-design.md
Normal file
338
docs/superpowers/specs/2026-06-13-moex-vibe-design.md
Normal 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 для масштабирования
|
||||
Loading…
x
Reference in New Issue
Block a user