# 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).