XIRR: bisection with a Descartes uniqueness check. More than one sign change returns AMBIGUOUS_RETURN (conservative, owner decision).
Period: starts at the first external flow (owner decision); under 365 days → basis=period.
Shadow portfolio: exact decimal units at the flow-day close (previous trading day on weekends); a negative position → index unavailable.
isCompleteValuation: extracted into portfolio domain and reused by value history.
Verification
npm run verify: 52 suites / 364 tests passed, lint and typecheck clean, nest build ok.
npm run openapi:check: matches runtime; npm run test:openapi-contract: 5/5.
Migration applied on a disposable SQLite DB (prisma migrate deploy); migrate diff from migrations to schema is empty.
Reference cases in tests: 10% over 730 days; 1000→1100 index gives 110 000 ₽; Friday close for a Saturday flow; two-root case → AMBIGUOUS_RETURN; period from first deposit (+10% over 100 days).
The regenerated openapi.json also drops the stale sync=observe query and tbank-sync/{id} that were already gone from runtime code; the frontend snapshot was simply behind.
Before closing the Issue, transfers must be checked manually on real data: UNCONFIRMED_TRANSFER excludes accounts until a confirmed link exists.
## Summary
Adds `GET /api/v1/brokerage-accounts/{accountId|all}/performance`:
- profit in ₽;
- XIRR return over all time;
- "the same money in the Moscow Exchange index" (MCFTRR) and outperformance; IMOEX as reference only.
Stores daily MOEX index closes locally.
## Linked Issue
WealthLedger/main#54 · spec: WealthLedger/specs#85 (change `2026-11-07-investment-performance-vs-index`)
## Changes
- **`market-index`**:
- `MarketIndexDailyClose`, `MarketIndexSyncState` (additive migration);
- `MoexClient.indexHistory` with strict pagination; one bad page means failure, no partial saves;
- confirmed coverage range;
- backward extension fetched immediately, tail and retry-after-failure at most hourly;
- single-flight;
- logs `integration.moex.index_history.*`.
- **`performance`**:
- external flows: RUB INPUT*/OUTPUT* only, cut off at `fetchedAt`;
- coverage check up to the valuation instant;
- reasons: `HISTORY_START_UNKNOWN`, `OPERATIONS_HISTORY_INCOMPLETE`, `PORTFOLIO_VALUATION_INCOMPLETE`, `NON_RUB_ACTIVITY`, `UNVALUED_SECURITIES_TRANSFER`, `UNCONFIRMED_TRANSFER`, `ACCOUNT_VALUATION_DATES_DIFFER`, `NO_SOLUTION`, `AMBIGUOUS_RETURN`, `INDEX_HISTORY_UNAVAILABLE`.
- **XIRR**: bisection with a Descartes uniqueness check. More than one sign change returns `AMBIGUOUS_RETURN` (conservative, owner decision).
- **Period**: starts at the first external flow (owner decision); under 365 days → `basis=period`.
- **Shadow portfolio**: exact decimal units at the flow-day close (previous trading day on weekends); a negative position → index unavailable.
- **`isCompleteValuation`**: extracted into portfolio domain and reused by value history.
## Verification
- `npm run verify`: 52 suites / 364 tests passed, lint and typecheck clean, `nest build` ok.
- `npm run openapi:check`: matches runtime; `npm run test:openapi-contract`: 5/5.
- Migration applied on a disposable SQLite DB (`prisma migrate deploy`); `migrate diff` from migrations to schema is empty.
- Reference cases in tests: 10% over 730 days; 1000→1100 index gives 110 000 ₽; Friday close for a Saturday flow; two-root case → `AMBIGUOUS_RETURN`; period from first deposit (+10% over 100 days).
- Independent review done; findings fixed (coverage throttling, unpublished tail close, period basis, negative units, missing-amount reason).
## Notes
- The regenerated `openapi.json` also drops the stale `sync=observe` query and `tbank-sync/{id}` that were already gone from runtime code; the frontend snapshot was simply behind.
- Before closing the Issue, transfers must be checked manually on real data: `UNCONFIRMED_TRANSFER` excludes accounts until a confirmed link exists.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Adds
GET /api/v1/brokerage-accounts/{accountId|all}/performance:Stores daily MOEX index closes locally.
Linked Issue
WealthLedger/main#54 · spec: WealthLedger/specs#85 (change
2026-11-07-investment-performance-vs-index)Changes
market-index:MarketIndexDailyClose,MarketIndexSyncState(additive migration);MoexClient.indexHistorywith strict pagination; one bad page means failure, no partial saves;integration.moex.index_history.*.performance:fetchedAt;HISTORY_START_UNKNOWN,OPERATIONS_HISTORY_INCOMPLETE,PORTFOLIO_VALUATION_INCOMPLETE,NON_RUB_ACTIVITY,UNVALUED_SECURITIES_TRANSFER,UNCONFIRMED_TRANSFER,ACCOUNT_VALUATION_DATES_DIFFER,NO_SOLUTION,AMBIGUOUS_RETURN,INDEX_HISTORY_UNAVAILABLE.AMBIGUOUS_RETURN(conservative, owner decision).basis=period.isCompleteValuation: extracted into portfolio domain and reused by value history.Verification
npm run verify: 52 suites / 364 tests passed, lint and typecheck clean,nest buildok.npm run openapi:check: matches runtime;npm run test:openapi-contract: 5/5.prisma migrate deploy);migrate difffrom migrations to schema is empty.AMBIGUOUS_RETURN; period from first deposit (+10% over 100 days).Notes
openapi.jsonalso drops the stalesync=observequery andtbank-sync/{id}that were already gone from runtime code; the frontend snapshot was simply behind.UNCONFIRMED_TRANSFERexcludes accounts until a confirmed link exists.fc0da12032to42b1d60fba