75 lines
3.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Portfolio Enricher Optimization
**Date:** 2026-06-14
**Status:** Approved
**Author:** AI Agent
## Problem
`GET /api/v1/portfolios/1` с 104 позициями (90 облигаций + 14 акций) выполняется ~29 секунд из-за 298 последовательных HTTP-запросов к MOEX ISS через rate limiter (10 req/s).
## Root Cause
Per-position enrichment в `PortfolioService.enrichPositions()` генерирует:
| Шаг | Вызовов | Метод |
|---|---|---|
| shortName | 104 | `getSecurityDescription`**избыточно** |
| Акции (14) | 14 | `getShareMarketData` |
| Облигации (90) | 90 | `getBondData` |
| Облигации (90) | 90 | `getBondMarketData`**дублирует endpoint** |
| **Total** | **298** | |
Две ключевые проблемы:
1. `getBondData` и `getBondMarketData` вызывают **один и тот же** MOEX endpoint, но парсят разные таблицы ответа
2. `getSecurityDescription` для shortName — избыточен: shortName уже доступен в market data ответах
3. Каждый secid запрашивается отдельно, хотя MOEX ISS поддерживает batch через `?securities=` параметр
## Solution
### 1. Merge bond data calls
Новый метод `getBondDataCombined(secid)` делает один запрос к MOEX и парсит обе таблицы (`securities` + `marketdata`), возвращая объединённый результат.
**Profit:** 180 → 90 запросов для bonds
### 2. Remove redundant getSecurityDescription
- `getShareMarketData` response уже содержит `SHORTNAME` в `securities` таблице — добавим поле `shortName` в тип `MoexShareMarketData`
- `getBondDataCombined` уже возвращает shortName из `securities` таблицы
**Profit:** 104 → 0 запросов
### 3. Batch requests by market
Группируем secid по типу (share/bond) и делаем 2 batch-запроса вместо индивидуальных:
- `GET /engines/stock/markets/shares/securities.json?securities=SBER,VTBR,...&boards=TQBR`
- `GET /engines/stock/markets/bonds/securities.json?securities=RU000A...,SU26240...&boards=TQCB`
Новые методы: `getShareMarketDataBatch(secids)`, `getBondMarketDataBatch(secids)`.
**Profit:** 104 → 2 запроса
### Metrics
| Scenario | API calls | Est. time (10 req/s) |
|---|---|---|
| Before | 298 | ~29.8s |
| After | 2 | ~0.3s |
## Files Changed
| File | Change |
|---|---|
| `apps/backend/src/modules/moex-client/moex-client.types.ts` | Add `shortName` to `MoexShareMarketData` |
| `apps/backend/src/modules/moex-client/moex-client.service.ts` | +`getShareMarketDataBatch`, +`getBondMarketDataBatch`, merge bond methods, add `shortName` to share response |
| `apps/backend/src/modules/portfolio/portfolio.service.ts` | Rewrite `enrichPositions` — batch, no redundant calls |
## Risks and Mitigations
- **MOEX ISS rate limiting**: Batch reduces requests, lowering risk. Circuit breaker stays intact.
- **Cache invalidation**: Batch results cached per market, not per secid. TTL unchanged (900s market data).
- **Empty batches**: If portfolio has no shares or no bonds, skip market entirely. No unnecessary calls.
- **Long secid lists**: URL length may exceed limits. Mitigation: split batches if secids > 50 per call (monitor and split if needed).