moex-vibe/docs/superpowers/adr/2026-06-14-portfolio-enricher-optimization.md
Sergey Krylov 60e456fbb5 perf: reduce portfolio enrichment from 298 to 2 MOEX API calls (-99.3%)
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
2026-06-14 13:19:22 +03:00

3.0 KiB
Raw Blame History

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