# ADR: Portfolio Enricher Optimization **Date:** 2026-06-14 **Status:** Implemented **Deciders:** AI Agent + Human ## Context `GET /api/v1/portfolios/1` выполнялся ~29 секунд для портфеля с 104 позициями. Причина: per-position enrichment генерировал 298 последовательных HTTP-запросов к MOEX ISS через rate limiter (10 req/s). ## Decision Три оптимизации, реализованные одновременно: ### 1. Merge bond data calls `getBondData` и `getBondMarketData` вызывали **один и тот же** MOEX endpoint (`/engines/stock/markets/bonds/securities/{secid}`), но парсили разные таблицы ответа. Новый метод `getBondPositionDataBatch` делает один запрос на все облигации и парсит обе таблицы. **Profit:** 180 → 90 запросов для bonds ### 2. Remove redundant `getSecurityDescription` Каждая позиция делала отдельный запрос для shortName. Но shortName уже доступен: - в `securities` таблице ответа `getShareMarketData` - в `getBondData` / `getBondPositionDataBatch` Удалили вызов `getSecurityDescription` из `enrichPositions`. **Profit:** 104 → 0 запросов ### 3. Batch requests by market Вместо N индивидуальных запросов — группируем secid по типу и делаем 2 batch-запроса: - `GET /engines/stock/markets/shares/securities.json?securities=SBER,VTBR,...` - `GET /engines/stock/markets/bonds/securities.json?securities=RU000...,SU262...` Новые методы: `getShareMarketDataBatch`, `getBondPositionDataBatch`. **Profit:** 104 → 2 запроса ## Results | Metric | Before | After | Reduction | |---|---|---|---| | API calls to MOEX | 298 | 2 | **99.3%** | | Estimated latency (cache cold) | ~29.8s | ~0.3s | **99%** | | Code in PortfolioService | ~150 lines | ~90 lines | **40%** | ## Consequences - **Cache key format changed**: from `marketdata:portfolio:{secid}` / `bonddata:portfolio:{secid}` / `security:portfolio-name:{secid}` to `batchdata:shares:{sortedSecids}` / `batchdata:bonds:{sortedSecids}`. Old cache entries will naturally expire via TTL. - **Cache granularity**: batch results are cached as a unit. If portfolio positions change, the cache key changes (because sorted secids change), triggering a fresh fetch. - **Backward compatibility**: `getShareMarketData(secid)` and `getBondData(secid)` + `getBondMarketData(secid)` are preserved for other consumers. ## Files Changed | File | Change | |---|---| | `moex-client.types.ts` | Added `shortName` to `MoexShareMarketData`, added `MoexBondPositionData` | | `moex-client.service.ts` | Added `getShareMarketDataBatch`, `getBondPositionDataBatch`, added `shortName` to `getShareMarketData` | | `portfolio.service.ts` | Rewrote `enrichPositions` to batch, removed redundant `getSecurityDescription` calls, removed old per-position enrichment methods |