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
66 lines
3.0 KiB
Markdown
66 lines
3.0 KiB
Markdown
# 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 |
|