Three optimizations: 1. Merge getBondData + getBondMarketData into single batch call (same endpoint, parse both tables) 2. Remove redundant getSecurityDescription for shortName (shortName already in market data responses) 3. Batch by market: 1 call for all shares, 1 call for all bonds (instead of N individual calls) Before: 298 API calls for 104 positions -> ~29.8s After: 2 API calls for 104 positions -> ~0.3s
3.0 KiB
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}tobatchdata: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)andgetBondData(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 |