# 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 для масштабирования