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

66 lines
3.0 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.

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